From ccb19be9d0e918070029f31a86c7eb546121bd87 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Mon, 27 May 2024 14:20:08 -0400 Subject: Updated docs --- src/codec.rs | 24 +++++-- src/jpeg/mod.rs | 2 + src/jpeg/segment.rs | 81 ++++++++++++++++++++++++ src/jpg/mod.rs | 2 - src/jpg/segment.rs | 66 -------------------- src/lib.rs | 14 ++++- src/lossless/champleve.rs | 139 ----------------------------------------- src/lossless/lsb.rs | 156 ++++++++++++++++++++++++++++++++++++++++++++++ src/lossless/mod.rs | 4 +- 9 files changed, 270 insertions(+), 218 deletions(-) create mode 100644 src/jpeg/mod.rs create mode 100644 src/jpeg/segment.rs delete mode 100644 src/jpg/mod.rs delete mode 100644 src/jpg/segment.rs delete mode 100644 src/lossless/champleve.rs create mode 100644 src/lossless/lsb.rs (limited to 'src') diff --git a/src/codec.rs b/src/codec.rs index 03cdf15..8b38a9b 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -1,14 +1,26 @@ +/// Codecs enable the concealment of payload data inside the data of a carrier. pub trait Codec { + /// Data type representing the carrier. type Carrier; + + /// Data type representing the payload. type Payload; + + /// Data type representing encoder output/decoder input (usually the same as the carrier). type Output; + + /// Type of errors produced by this codec. type Error; - fn encode( - &self, - carrier: impl Into, - payload: impl Into, - ) -> Result; + /// Embeds payload data inside carrier, returning the result. + fn encode(&self, carrier: C, payload: P) -> Result + where + C: Into, + P: Into; - fn decode(&self, encoded: impl Into) -> Result<(Self::Carrier, Self::Payload), Self::Error>; + /// Extracts payload data from an encoded carrier, returning the carrier with data removed and the + /// payload data. + fn decode(&self, encoded: E) -> Result<(Self::Carrier, Self::Payload), Self::Error> + where + E: Into; } diff --git a/src/jpeg/mod.rs b/src/jpeg/mod.rs new file mode 100644 index 0000000..3d6bdc3 --- /dev/null +++ b/src/jpeg/mod.rs @@ -0,0 +1,2 @@ +mod segment; +pub use segment::*; diff --git a/src/jpeg/segment.rs b/src/jpeg/segment.rs new file mode 100644 index 0000000..b20ba3b --- /dev/null +++ b/src/jpeg/segment.rs @@ -0,0 +1,81 @@ +use std::{mem::size_of, usize}; + +use img_parts::jpeg::{markers, Jpeg, JpegSegment}; +use thiserror::Error; + +use crate::codec::Codec; + +/// 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. +#[derive(Debug, PartialEq, Eq)] +pub struct JpegSegmentCodec { + /// Index of segment to insert comments at. + pub start_index: usize, +} + +impl Codec for JpegSegmentCodec { + type Carrier = Vec; + type Payload = Vec; + type Output = Self::Carrier; + type Error = JpegSegmentError; + + fn encode(&self, carrier: C, payload: P) -> Result + where + C: Into, + P: Into, + { + let mut jpeg = match Jpeg::from_bytes(carrier.into().into()) { + Ok(image) => image, + Err(err) => return Err(JpegSegmentError::ParseFailed { inner: err }) + }; + let mut payload_bytes: Self::Carrier = payload.into(); + let segment_count = ((payload_bytes.len() + size_of::()) as u64).div_ceil((u16::MAX as usize - size_of::()) as u64); + payload_bytes.splice(0..0, segment_count.to_le_bytes()); + for (index, payload_chunk) in payload_bytes.chunks(u16::MAX as usize - size_of::()).enumerate() { + let segment = JpegSegment::new_with_contents(markers::COM, payload_chunk.to_vec().into()); + jpeg.segments_mut().insert(self.start_index + index, segment); + } + Ok(jpeg.encoder().bytes().to_vec()) + } + + fn decode(&self, encoded: E) -> Result<(Self::Carrier, Self::Payload), Self::Error> + where + E: Into, + { + let mut jpeg = match Jpeg::from_bytes(encoded.into().into()) { + Ok(image) => image, + Err(err) => return Err(JpegSegmentError::ParseFailed { inner: err }) + }; + let segment = jpeg.segments_mut().remove(self.start_index); + let segment_bytes = segment.contents(); + let segment_count = u64::from_le_bytes(segment_bytes[0..size_of::()].try_into().unwrap()) as usize; + let mut payload_vec: Vec = Vec::with_capacity((u16::MAX as usize - size_of::()) * segment_count); + payload_vec.extend(segment_bytes[size_of::()..].to_vec()); + + for _ in 0..segment_count-1 { + let segment = jpeg.segments_mut().remove(self.start_index); + payload_vec.extend(segment.contents()); + } + + Ok((jpeg.encoder().bytes().to_vec(), payload_vec)) + } +} + +impl Default for JpegSegmentCodec { + fn default() -> Self { + Self { + start_index: 3, + } + } +} + +/// Errors thrown by the JPEG segment codec. +#[derive(Error, Debug)] +pub enum JpegSegmentError { + /// Parsing JPEG data failed. + #[error("Failed to parse JPEG data: {inner:?}")] + ParseFailed { + /// Error thrown by parser. + inner: img_parts::Error, + } +} diff --git a/src/jpg/mod.rs b/src/jpg/mod.rs deleted file mode 100644 index 3d6bdc3..0000000 --- a/src/jpg/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod segment; -pub use segment::*; diff --git a/src/jpg/segment.rs b/src/jpg/segment.rs deleted file mode 100644 index f54d0e2..0000000 --- a/src/jpg/segment.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{mem::size_of, usize}; - -use img_parts::jpeg::{markers, Jpeg, JpegSegment}; -use thiserror::Error; - -use crate::codec::Codec; - -#[derive(Debug, PartialEq, Eq)] -pub struct JpegSegmentCodec { - pub start_index: usize, -} - -impl Codec for JpegSegmentCodec { - type Carrier = Vec; - type Payload = Vec; - type Output = Self::Carrier; - type Error = JpegSegmentError; - - fn encode(&self, carrier: impl Into, payload: impl Into) -> Result { - let mut jpeg = match Jpeg::from_bytes(carrier.into().into()) { - Ok(image) => image, - Err(err) => return Err(JpegSegmentError::ParseFailed { inner: err }) - }; - let mut payload_bytes: Self::Carrier = payload.into(); - let segment_count = ((payload_bytes.len() + size_of::()) as u64).div_ceil((u16::MAX as usize - size_of::()) as u64); - payload_bytes.splice(0..0, segment_count.to_le_bytes()); - for (index, payload_chunk) in payload_bytes.chunks(u16::MAX as usize - size_of::()).enumerate() { - let segment = JpegSegment::new_with_contents(markers::COM, payload_chunk.to_vec().into()); - jpeg.segments_mut().insert(self.start_index + index, segment); - } - Ok(jpeg.encoder().bytes().to_vec()) - } - - fn decode(&self, encoded: impl Into) -> Result<(Self::Carrier, Self::Payload), Self::Error> { - let mut jpeg = match Jpeg::from_bytes(encoded.into().into()) { - Ok(image) => image, - Err(err) => return Err(JpegSegmentError::ParseFailed { inner: err }) - }; - let segment = jpeg.segments_mut().remove(self.start_index); - let segment_bytes = segment.contents(); - let segment_count = u64::from_le_bytes(segment_bytes[0..size_of::()].try_into().unwrap()) as usize; - let mut payload_vec: Vec = Vec::with_capacity((u16::MAX as usize - size_of::()) * segment_count); - payload_vec.extend(segment_bytes[size_of::()..].to_vec()); - - for _ in 0..segment_count-1 { - let segment = jpeg.segments_mut().remove(self.start_index); - payload_vec.extend(segment.contents()); - } - - Ok((jpeg.encoder().bytes().to_vec(), payload_vec)) - } -} - -impl Default for JpegSegmentCodec { - fn default() -> Self { - Self { - start_index: 3, - } - } -} - -#[derive(Error, Debug)] -pub enum JpegSegmentError { - #[error("Failed to parse JPEG data: {inner:?}")] - ParseFailed { inner: img_parts::Error } -} diff --git a/src/lib.rs b/src/lib.rs index a1ed909..7cbae7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,15 @@ -pub mod codec; +#![warn(missing_docs)] -#[cfg(feature = "jpg")] -pub mod jpg; +//! Library providing steganography codecs for various carrier and payload types, designed to be +//! extensible. +mod codec; +pub use codec::*; + +/// Codecs for carriers in JPEG format. +#[cfg(feature = "jpeg")] +pub mod jpeg; + +/// Codecs for carriers in lossless image formats (PNG, WebP, etc.). #[cfg(feature = "lossless")] pub mod lossless; diff --git a/src/lossless/champleve.rs b/src/lossless/champleve.rs deleted file mode 100644 index 948408c..0000000 --- a/src/lossless/champleve.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::cmp::Ordering; - -use image::{ColorType, DynamicImage, GenericImageView, Pixel}; -use thiserror::Error; - -use crate::codec::Codec; - -#[derive(Debug)] -pub struct ChampleveCodec; - -impl Codec for ChampleveCodec { - type Carrier = DynamicImage; - type Payload = Vec; - type Output = Self::Carrier; - type Error = ChampleveError; - - fn encode(&self, carrier: impl Into, payload: impl Into) -> Result { - let mut image: DynamicImage = carrier.into(); - let payload: Vec = payload.into(); - - if image.pixels().count() < payload.len() { - return Err(ChampleveError::PayloadTooBig); - } - - let mut payload_iter = payload.iter(); - - match image { - DynamicImage::ImageRgba8(ref mut image) => { - for pixel in image.pixels_mut() { - if let Some(payload_byte) = payload_iter.next() { - encode_pixel(pixel, *payload_byte, false); - } else { - encode_pixel(pixel, 0, true); - } - } - }, - DynamicImage::ImageRgb8(ref mut image) => { - for pixel in image.pixels_mut() { - if let Some(payload_byte) = payload_iter.next() { - encode_pixel(pixel, *payload_byte, false); - } else { - encode_pixel(pixel, 0, true); - } - } - }, - _ => return Err(ChampleveError::UnsupportedFormat { format: image.color() }) - } - - Ok(image) - } - - fn decode(&self, carrier: impl Into) -> Result<(Self::Carrier, Self::Payload), ChampleveError> { - let mut image: DynamicImage = carrier.into(); - let mut payload: Vec = Vec::new(); - - match image { - DynamicImage::ImageRgba8(ref mut image) => { - for pixel in image.pixels_mut() { - if let Some(payload_byte) = decode_pixel(pixel) { - payload.push(payload_byte); - } else { - break; - } - } - }, - DynamicImage::ImageRgb8(ref mut image) => { - for pixel in image.pixels_mut() { - if let Some(payload_byte) = decode_pixel(pixel) { - payload.push(payload_byte); - } else { - break; - } - } - }, - _ => return Err(ChampleveError::UnsupportedFormat { format: image.color() }) - } - - Ok((image, payload)) - } -} - -fn encode_pixel>(pixel: &mut P, payload_byte: u8, end_of_data: bool) { - let mut bits_remaining: i32 = 8; - for channel in pixel.channels_mut() { - *channel &= 0b11111000; - bits_remaining -= 3; - if bits_remaining <= -3 { - break; - } - - let mask = match bits_remaining.cmp(&0) { - Ordering::Less => payload_byte << -bits_remaining, - _ => payload_byte >> bits_remaining, - } & 0b00000111; - - *channel |= mask; - } - - // Add end-of-data marker to final bit if necessary - if end_of_data { - *pixel.channels_mut().last_mut().unwrap() |= 1; - } -} - -fn decode_pixel>(pixel: &mut P) -> Option { - - // Final bit as end-of-data marker - if pixel.channels().last().unwrap() & 1 == 1 { - return None; - } - - let mut bits_remaining: i32 = 8; - let mut payload_byte: u8 = 0; - for channel in pixel.channels_mut() { - bits_remaining -= 3; - if bits_remaining <= -3 { - break; - } - - let channel_bits = *channel & 0b00000111; - *channel &= 0b11111000; - let mask = match bits_remaining.cmp(&0) { - Ordering::Less => channel_bits >> -bits_remaining, - _ => channel_bits << bits_remaining, - }; - payload_byte |= mask; - } - Some(payload_byte) -} - -#[derive(Error, Debug)] -pub enum ChampleveError { - #[error("Payload is too big for the carrier. Choose a smaller payload or an image with greater pixel dimensions.")] - PayloadTooBig, - #[error("Specified image format ({format:?}) is unsupported.")] - UnsupportedFormat { - format: ColorType - }, -} diff --git a/src/lossless/lsb.rs b/src/lossless/lsb.rs new file mode 100644 index 0000000..59dcc0b --- /dev/null +++ b/src/lossless/lsb.rs @@ -0,0 +1,156 @@ +use std::cmp::Ordering; + +use image::{ColorType, DynamicImage, GenericImageView, Pixel}; +use thiserror::Error; + +use crate::codec::Codec; + +/// 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 +/// allow a byte of data to fit in each pixel of the image. 3 bits of data are encoded per pixel, +/// and the 9th bit is used to signal the end of data. +#[derive(Debug)] +pub struct LsbCodec; + +impl Codec for LsbCodec { + type Carrier = DynamicImage; + type Payload = Vec; + type Output = Self::Carrier; + type Error = LsbError; + + fn encode(&self, carrier: C, payload: P) -> Result + where + C: Into, + P: Into, + { + let mut image: DynamicImage = carrier.into(); + let payload: Vec = payload.into(); + + if image.pixels().count() < payload.len() { + return Err(LsbError::PayloadTooBig); + } + + let mut payload_iter = payload.iter(); + + match image { + DynamicImage::ImageRgba8(ref mut image) => { + for pixel in image.pixels_mut() { + if let Some(payload_byte) = payload_iter.next() { + encode_pixel(pixel, *payload_byte, false); + } else { + encode_pixel(pixel, 0, true); + } + } + }, + DynamicImage::ImageRgb8(ref mut image) => { + for pixel in image.pixels_mut() { + if let Some(payload_byte) = payload_iter.next() { + encode_pixel(pixel, *payload_byte, false); + } else { + encode_pixel(pixel, 0, true); + } + } + }, + _ => return Err(LsbError::UnsupportedFormat { format: image.color() }) + } + + Ok(image) + } + + fn decode(&self, carrier: E) -> Result<(Self::Carrier, Self::Payload), LsbError> + where + E: Into, + { + let mut image: DynamicImage = carrier.into(); + let mut payload: Vec = Vec::new(); + + match image { + DynamicImage::ImageRgba8(ref mut image) => { + for pixel in image.pixels_mut() { + if let Some(payload_byte) = decode_pixel(pixel) { + payload.push(payload_byte); + } else { + break; + } + } + }, + DynamicImage::ImageRgb8(ref mut image) => { + for pixel in image.pixels_mut() { + if let Some(payload_byte) = decode_pixel(pixel) { + payload.push(payload_byte); + } else { + break; + } + } + }, + _ => return Err(LsbError::UnsupportedFormat { format: image.color() }) + } + + Ok((image, payload)) + } +} + +fn encode_pixel>(pixel: &mut P, payload_byte: u8, end_of_data: bool) { + let mut bits_remaining: i32 = 8; + for channel in pixel.channels_mut() { + *channel &= 0b11111000; + bits_remaining -= 3; + if bits_remaining <= -3 { + break; + } + + let mask = match bits_remaining.cmp(&0) { + Ordering::Less => payload_byte << -bits_remaining, + _ => payload_byte >> bits_remaining, + } & 0b00000111; + + *channel |= mask; + } + + // Add end-of-data marker to final bit if necessary + if end_of_data { + *pixel.channels_mut().last_mut().unwrap() |= 1; + } +} + +fn decode_pixel>(pixel: &mut P) -> Option { + + // Final bit as end-of-data marker + if pixel.channels().last().unwrap() & 1 == 1 { + return None; + } + + let mut bits_remaining: i32 = 8; + let mut payload_byte: u8 = 0; + for channel in pixel.channels_mut() { + bits_remaining -= 3; + if bits_remaining <= -3 { + break; + } + + let channel_bits = *channel & 0b00000111; + *channel &= 0b11111000; + let mask = match bits_remaining.cmp(&0) { + Ordering::Less => channel_bits >> -bits_remaining, + _ => channel_bits << bits_remaining, + }; + payload_byte |= mask; + } + Some(payload_byte) +} + +/// Errors thrown by the LSB Codec. +#[derive(Error, Debug)] +pub enum LsbError { + + /// Error thrown when payload is too big for the carrier. + #[error("Payload is too big for the carrier. Choose a smaller payload or an image with greater pixel dimensions.")] + PayloadTooBig, + + /// Error thrown when pixel format is unsupported. + #[error("Specified image format ({format:?}) is unsupported.")] + UnsupportedFormat { + /// Provided (invalid) format. + format: ColorType + }, +} diff --git a/src/lossless/mod.rs b/src/lossless/mod.rs index a9ae225..a6bda54 100644 --- a/src/lossless/mod.rs +++ b/src/lossless/mod.rs @@ -1,2 +1,2 @@ -mod champleve; -pub use champleve::*; +mod lsb; +pub use lsb::*; -- cgit v1.2.3