From 7d8cb589ec23194f4648381c80acfbffdf8c0543 Mon Sep 17 00:00:00 2001 From: ef3d0c3e Date: Tue, 20 Aug 2024 16:04:49 +0200 Subject: [PATCH] Added embed entropy --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + README.md | 12 +++++++++- examples/test.sh | 2 +- src/png_data/header.rs | 19 ++++++++------- src/png_data/main.rs | 36 ++++++++++++++-------------- src/png_embed/block.rs | 32 +++++++++++++++++++++---- src/png_embed/ent.rs | 53 ++++++++++++++++++++++++++++++++++++++++++ src/png_embed/main.rs | 23 ++++++++++++++---- 9 files changed, 148 insertions(+), 37 deletions(-) create mode 100644 src/png_embed/ent.rs diff --git a/Cargo.lock b/Cargo.lock index df7af04..69e05b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "entropy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68716e45ef572f351be6fad93a7bbf35242b4289a2ff75434032e5d73d74cc2" + [[package]] name = "fdeflate" version = "0.3.4" @@ -230,6 +236,7 @@ dependencies = [ "argon2", "bitvec", "crc", + "entropy", "getopts", "png", "rand", diff --git a/Cargo.toml b/Cargo.toml index ba5b4ab..fdc163d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ path = "src/png_data/main.rs" argon2 = "0.5.3" bitvec = "1.0.1" crc = "3.2.1" +entropy = "0.4.2" getopts = "0.2.21" png = "0.17.13" rand = "0.8.5" diff --git a/README.md b/README.md index f224cc7..f3baa9e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![TeX Live english documentation](doc/texlive_en.png) -`png_data` transforms a file as a png image. +`png_data` encodes a file into a png image. ### Encoding `png_data -l rgb8 -e file.pdf -o output.png -c "(.pdf) documentation"` @@ -52,6 +52,10 @@ Where: * `output.png` the resulting PNG file * `"(.tar) archive"` an optional comment +**Additional Options** + * `-s|--seed TXT` Sets the random seed for determining the payload blocks. By default the random seed is "WIDTHxHEIGHT" where WIDTH and HEIGHT are the original image's dimensions. + * `-n|--entropy` Fills unused payload blocks with random data that tries to match the payload's entropy. This feature is experimental and may not fully protect against entropy based steganography-detection. We highlihy recommend that the payload has maximal entropy, which can be achieved by compressing it. + ### Decoding an image `png_embed -l lo2 -d image.png -o embed.tar` Where: @@ -59,12 +63,18 @@ Where: * `image.png` the PNG containing an embed * `embed.tar` the extracted embedded file +**Additional Options** + * `-s|--seed TXT` Sets the random seed for determining the payload blocks. By default the random seed is "WIDTHxHEIGHT" where WIDTH and HEIGHT are the original image's dimensions. + ### Getting header information `png_embed -l lo2 -z output.png` * `lo2` is the `Lo` algorithm using the 2 lowest bits * `output.png` a `png_embed` encoded image This will display the header of the encoded file, as well as the comment. +**Additional Options** + * `-s|--seed TXT` Sets the random seed for determining the payload blocks. By default the random seed is "WIDTHxHEIGHT" where WIDTH and HEIGHT are the original image's dimensions. + # License png_data is licensed under the GNU AGPL version 3 or later. See [LICENSE.md](LICENSE.md) for more information. diff --git a/examples/test.sh b/examples/test.sh index f05db6b..a398bf5 100755 --- a/examples/test.sh +++ b/examples/test.sh @@ -7,7 +7,7 @@ PNG_EMBED=../target/debug/png_embed echo "Encoding..." for i in {1..7}; do echo "Writing dec-lo${i}.." - $PNG_EMBED -l lo${i} -e embed.png input.png -o out-lo${i}.png + $PNG_EMBED -l lo${i} -n -e embed.png input.png -o out-lo${i}.png done echo "Decoding..." diff --git a/src/png_data/header.rs b/src/png_data/header.rs index 9b9121f..0376375 100644 --- a/src/png_data/header.rs +++ b/src/png_data/header.rs @@ -10,7 +10,8 @@ pub trait Decode { /// Decode the data from an iterator fn decode(it: &mut I) -> Result - where I: Iterator; + where + I: Iterator; } /// The program's version. @@ -69,7 +70,7 @@ impl Header { } impl Encode for Header { - fn encode(&self, vec: &mut Vec) { + fn encode(&self, vec: &mut Vec) { // Version vec.extend_from_slice((self.version as u16).to_le_bytes().as_slice()); @@ -87,14 +88,16 @@ impl Encode for Header { if let Some(comment) = &self.comment { vec.extend_from_slice(comment.as_bytes()); } - } + } } impl Decode for Header { - type Type = Header; + type Type = Header; - fn decode(it: &mut I) -> Result - where I: Iterator { + fn decode(it: &mut I) -> Result + where + I: Iterator, + { let mut count = 0; let mut next = || -> Result { let result = it @@ -117,7 +120,7 @@ impl Decode for Header { Some( String::from_utf8(comment_data) - .map_err(|e| format!("Failed to retrieve comment: {e}"))?, + .map_err(|e| format!("Failed to retrieve comment: {e}"))?, ) } else { None @@ -129,7 +132,7 @@ impl Decode for Header { data_crc, comment, }) - } + } } /* diff --git a/src/png_data/main.rs b/src/png_data/main.rs index 0c63131..ac2fdad 100644 --- a/src/png_data/main.rs +++ b/src/png_data/main.rs @@ -45,7 +45,7 @@ fn str_to_layout(layout: &str) -> Result<(ColorType, BitDepth), String> { let split = layout .char_indices() .find(|(_, c)| c.is_ascii_digit()) - .ok_or(format!("Unable to find number for layout's bit depth"))? + .ok_or("Unable to find number for layout's bit depth".to_string())? .0; match layout.split_at(split) { ("rgb", bits) => match bits { @@ -99,8 +99,7 @@ fn best_layout(size: u64, bits_per_pixel: u8) -> (u32, u32) { } /// Gets the minimum image buffer size in bytes -fn minimum_size(color: ColorType, depth: BitDepth, width: u32, height: u32) -> usize -{ +fn minimum_size(color: ColorType, depth: BitDepth, width: u32, height: u32) -> usize { let samples = width as usize * color.samples(); (match depth { BitDepth::Sixteen => samples * 2, @@ -137,15 +136,12 @@ fn encode(input: String, output: String, layout: String, matches: Matches) -> Re eprintln!("=============="); let bits_per_pixel = bits_per_pixel(layout.0, layout.1); - let (width, height) = best_layout( - (data.len() + input_data.len()) as u64, - bits_per_pixel - ); + let (width, height) = best_layout((data.len() + input_data.len()) as u64, bits_per_pixel); // Encode let output_file = File::create(&output) .map_err(|err| format!("Failed to open output file `{output}`: {err}"))?; - let ref mut w = BufWriter::new(output_file); + let w = &mut BufWriter::new(output_file); let mut encoder = png::Encoder::new(w, width, height); encoder.set_color(layout.0); encoder.set_depth(layout.1); @@ -186,12 +182,12 @@ fn decode_header(input: String, _matches: Matches) -> Result<(), String> { let info = reader .next_frame(data.as_mut_slice()) .map_err(|err| format!("Failed to read png info for `{input}`: {err}"))?; - + data.resize(info.buffer_size(), 0); - let mut it = data.iter().enumerate().map(|(idx, byte)| (idx, *byte)); - let header = Header::decode(&mut it).map_err(|err| format!("Failed to decode header: {err}"))?; + let header = + Header::decode(&mut it).map_err(|err| format!("Failed to decode header: {err}"))?; eprintln!("=== HEADER ==="); eprintln!("Version: {:#?}", header.version); eprintln!( @@ -216,13 +212,11 @@ fn decode(input: String, output: String, _matches: Matches) -> Result<(), String let info = reader .next_frame(data.as_mut_slice()) .map_err(|err| format!("Failed to read png info for `{input}`: {err}"))?; - + data.resize(info.buffer_size(), 0); - let mut it = data.iter().enumerate().map(|(idx, byte)| (idx, *byte)); - let header = - { + let header = { //let mut temp_it = std::mem::take(&mut it); Header::decode(&mut it).map_err(|err| format!("Failed to decode header: {err}"))? }; @@ -236,14 +230,18 @@ fn decode(input: String, output: String, _matches: Matches) -> Result<(), String eprintln!("=============="); // Check crc - let data_start = it.next().ok_or(format!("Failed to get data start"))?.0; - let file_data = &data[data_start..data_start+header.data_len as usize]; + let data_start = it.next().ok_or("Failed to get data start".to_string())?.0; + let file_data = &data[data_start..data_start + header.data_len as usize]; let crc = Crc::::new(&crc::CRC_32_CKSUM).checksum(file_data); if crc != header.data_crc { - Err(format!("Data CRC[{crc:X}] does not match header CRC[{:X}]", header.data_crc))?; + Err(format!( + "Data CRC[{crc:X}] does not match header CRC[{:X}]", + header.data_crc + ))?; } - std::fs::write(&output, file_data).map_err(|err| format!("Failed to write to output file `{output}`: {err}"))?; + std::fs::write(&output, file_data) + .map_err(|err| format!("Failed to write to output file `{output}`: {err}"))?; println!("File written to `{output}`"); Ok(()) diff --git a/src/png_embed/block.rs b/src/png_embed/block.rs index 3e5f9d3..e531f3d 100644 --- a/src/png_embed/block.rs +++ b/src/png_embed/block.rs @@ -4,6 +4,7 @@ use rand::prelude::SliceRandom; use rand::Rng; use crate::embed::EmbedAlgorithm; +use crate::ent::EntropyGenerator; /// Gets the best blocksize (i.e. that minimize remaining space) for a certain data length. /// The blocksize is a number in range [16, 65536] @@ -27,6 +28,7 @@ pub struct BlockPlacement<'a> { algorithm: &'a EmbedAlgorithm, data: &'a mut [u8], block_size: usize, + pub used_blocks: usize, pub blocks: Vec, } @@ -68,14 +70,16 @@ impl<'a> BlockPlacement<'a> { blocks.shuffle(rng); // Only keep the first blocks_num blocks - blocks.resize(blocks_num, 0); + //blocks.resize(blocks_num, 0); - Ok(Self { + let s = Self { algorithm, data, block_size, + used_blocks: blocks_num, blocks, - }) + }; + Ok(s) } // Embeds the data into the original image @@ -93,7 +97,7 @@ impl<'a> BlockPlacement<'a> { let mut index = 0; match self.algorithm { EmbedAlgorithm::Lo(bits) => { - for block in &self.blocks { + for block in &self.blocks[0..self.used_blocks] { for i in 0..self.block_size { let pos = block * self.block_size + i; let hi = std::cmp::min(*bits as usize, embed.len() - index); @@ -107,6 +111,26 @@ impl<'a> BlockPlacement<'a> { } } } + + /// Fills unused blocks with randomly generated data + pub fn fill_unused(&mut self, mut gen: EntropyGenerator) + where + R: Rng, + { + match self.algorithm { + EmbedAlgorithm::Lo(bits) => { + let mask: u8 = (1 << bits) - 1; + for block in &self.blocks[self.used_blocks..] { + for i in 0..self.block_size { + let pos = block * self.block_size + i; + + self.data[pos] &= !mask; + self.data[pos] |= gen.next() & mask; + } + } + } + } + } } // Iterator over blocks in the resulting image diff --git a/src/png_embed/ent.rs b/src/png_embed/ent.rs new file mode 100644 index 0000000..ee73729 --- /dev/null +++ b/src/png_embed/ent.rs @@ -0,0 +1,53 @@ +use rand::distributions::WeightedIndex; +use rand::prelude::Distribution; +use rand::Rng; + +pub struct EntropyGenerator +where + R: Rng, +{ + rng: R, + dist: WeightedIndex, +} + +/// Genrates random bytes with a set entropy +impl EntropyGenerator { + // FIXME: Bad entropy + pub fn new(entropy: f64, rng: R) -> Self { + // FIXME: Does not work for entropy below 1.0 + let n = (2.0f64.powf(entropy)).round() as usize; + + let mut probabilities = std::iter::repeat(1.0f64).take(n).collect::>(); + let sum = probabilities.iter().sum::(); + probabilities.iter_mut().for_each(|p| *p /= sum); + + let dist = WeightedIndex::new(&probabilities).unwrap(); + + Self { rng, dist } + } + + pub fn next(&mut self) -> u8 { self.dist.sample(&mut self.rng) as u8 } +} + +#[cfg(test)] +mod tests { + use entropy::shannon_entropy; + use rand::SeedableRng; + use rand_chacha::ChaCha8Rng; + + use super::*; + + #[test] + fn test_entropy() { + for i in 1..8 { + let mut gen = EntropyGenerator::new(i as f64, ChaCha8Rng::from_entropy()); + + let mut data = Vec::with_capacity(1024); + for _ in 0..1024 { + data.push(gen.next()); + } + + assert!((shannon_entropy(data) - i as f32).abs() < 0.2); + } + } +} diff --git a/src/png_embed/main.rs b/src/png_embed/main.rs index 2e05b33..9ea35f3 100644 --- a/src/png_embed/main.rs +++ b/src/png_embed/main.rs @@ -1,7 +1,8 @@ -pub mod block; -pub mod embed; -pub mod header; -pub mod image; +mod block; +mod embed; +mod ent; +mod header; +mod image; use std::env; use std::fs::File; @@ -16,6 +17,7 @@ use block::BlockPlacement; use block::BlockPlacementIterator; use crc::Crc; use embed::EmbedAlgorithm; +use ent::EntropyGenerator; use getopts::Matches; use getopts::Options; use header::Decode; @@ -152,6 +154,14 @@ fn encode( eprintln!("=============="); placement.write_embed(embed_data.as_slice().view_bits::()); + if matches.opt_present("n") { + let ent = entropy::shannon_entropy(&embed_data); + println!("Payload entropy: {ent}\nFilling image remainder with random data..."); + placement.fill_unused(EntropyGenerator::new( + ent as f64, + ChaCha8Rng::from_entropy(), + )) + } let outfile = File::create(&output).unwrap(); let w = &mut BufWriter::new(Box::new(outfile) as Box); @@ -256,6 +266,11 @@ fn main() -> ExitCode { ); opts.optflag("z", "info", "Read header"); opts.optopt("l", "algorithm", "Embed algorithm", "lo3"); + opts.optflag( + "n", + "entropy", + "Attempts to hide payload by modifying the file's entropy", + ); opts.optflag("h", "help", "Print this help menu"); opts.optflag("v", "version", "Print program version and licenses");