From 45af81e8d57783c9f7c51ed97a7b3990ffd0ba75 Mon Sep 17 00:00:00 2001 From: Sven Assmann Date: Thu, 9 Dec 2021 21:45:30 +0100 Subject: [PATCH] feat(image:encoder): variable step rate for color channels - variable step by the encoder would move through the color channels - option for enable/disable the skip of the alpha channel for encoding --- .deb/Makefile | 1 + .github/workflows/build.yml | 6 +- .github/workflows/release-binary-assets.yml | 16 ++- .github/workflows/release.yml | 1 - Cargo.lock | 38 +++++- README.md | 4 + stegano-cli/Cargo.toml | 10 +- stegano-cli/src/main.rs | 32 ++++- stegano-core/Cargo.toml | 28 +++- stegano-core/benches/audio/decoding.rs | 21 +++ stegano-core/benches/audio/encoding.rs | 21 +++ stegano-core/benches/decoder_benchmark.rs | 43 ------ stegano-core/benches/encoder_benchmark.rs | 40 ------ stegano-core/benches/image/decoding.rs | 22 ++++ stegano-core/benches/image/encoding.rs | 22 ++++ stegano-core/src/commands.rs | 12 +- stegano-core/src/encoder.rs | 19 --- stegano-core/src/lib.rs | 120 ++++++++++++++--- stegano-core/src/media/audio/lsb_codec.rs | 7 +- stegano-core/src/media/audio/wav_iter.rs | 4 +- stegano-core/src/media/image/decoder.rs | 76 +++++------ stegano-core/src/media/image/encoder.rs | 104 ++++++++++++--- stegano-core/src/media/image/iterators.rs | 117 +++++++++++++++-- stegano-core/src/media/image/lsb_codec.rs | 75 +++++++++-- stegano-core/src/media/image/mod.rs | 2 +- stegano-core/src/message.rs | 7 +- stegano-core/src/universal_decoder.rs | 22 +++- stegano-core/src/universal_encoder.rs | 137 +++++++++++++++++--- 28 files changed, 751 insertions(+), 256 deletions(-) create mode 100644 stegano-core/benches/audio/decoding.rs create mode 100644 stegano-core/benches/audio/encoding.rs delete mode 100644 stegano-core/benches/decoder_benchmark.rs delete mode 100644 stegano-core/benches/encoder_benchmark.rs create mode 100644 stegano-core/benches/image/decoding.rs create mode 100644 stegano-core/benches/image/encoding.rs delete mode 100644 stegano-core/src/encoder.rs diff --git a/.deb/Makefile b/.deb/Makefile index 4c9bf66..73082aa 100644 --- a/.deb/Makefile +++ b/.deb/Makefile @@ -1,5 +1,6 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) deb: + docker pull 5422m4n/rust-deb-builder docker run --rm -v ${ROOT_DIR}/..:/mnt -w /mnt \ 5422m4n/rust-deb-builder \ cargo deb -p stegano-cli --target=x86_64-unknown-linux-musl \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b9f7ab7..3b8b5a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,12 +117,12 @@ jobs: name: binaray package .deb needs: check runs-on: ubuntu-latest - container: - image: 5422m4n/rust-deb-builder:latest steps: - uses: actions/checkout@v2 - name: cargo deb - run: cargo deb -p stegano-cli --target=x86_64-unknown-linux-musl + uses: sassman/rust-deb-builder@v1.57.0 + with: + package: stegano-cli - name: Archive deb artifact uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/release-binary-assets.yml b/.github/workflows/release-binary-assets.yml index e0b2bf0..579defe 100644 --- a/.github/workflows/release-binary-assets.yml +++ b/.github/workflows/release-binary-assets.yml @@ -68,8 +68,20 @@ jobs: omitBodyDuringUpdate: true omitNameDuringUpdate: true omitPrereleaseDuringUpdate: true - - run: cargo deb -p stegano-cli + - name: cargo deb if: ${{ matrix.os == 'ubuntu-latest' }} + uses: sassman/rust-deb-builder@v1.57.0 + with: + package: stegano-cli + - name: rename package + id: debpkg + shell: bash + env: + TAG: ${{ github.event.release.tag_name }} + run: | + filename="stegano-$TAG-amd64-static.deb" + mv target/x86_64-unknown-linux-musl/debian/stegano-cli*.deb "$filename" + echo "::set-output name=filename::$filename" - name: Upload Deb File if: ${{ matrix.os == 'ubuntu-latest' }} uses: ncipollo/release-action@v1.8.7 @@ -77,7 +89,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} allowUpdates: true artifactErrorsFailBuild: true - artifacts: ./target/debian/stegano-${{ steps.tag_name.outputs.current_version }}-amd64.deb + artifacts: ${{ steps.debpkg.outputs.filename }} artifactContentType: application/vnd.debian.binary-package omitBodyDuringUpdate: true omitNameDuringUpdate: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d886583..b45ac83 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -133,7 +133,6 @@ jobs: default: true profile: minimal - uses: Swatinem/rust-cache@v1 - - run: cargo install cargo-deb - name: Get version from tag id: tag_name run: echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} diff --git a/Cargo.lock b/Cargo.lock index 5cb5ac6..4994af7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,9 +16,9 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ansi_term" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ "winapi", ] @@ -103,6 +103,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cargo-husky" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad" + [[package]] name = "cast" version = "0.2.7" @@ -126,9 +132,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.33.3" +version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", @@ -281,6 +287,18 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "enum_dispatch" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd53b3fde38a39a06b2e66dc282f3e86191e53bd04cc499929c15742beae3df8" +dependencies = [ + "once_cell", + "proc-macro2 1.0.32", + "quote 1.0.10", + "syn 1.0.81", +] + [[package]] name = "flate2" version = "1.0.22" @@ -493,6 +511,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + [[package]] name = "oorandom" version = "11.1.3" @@ -785,7 +809,7 @@ dependencies = [ [[package]] name = "stegano-cli" -version = "0.4.10" +version = "0.5.0" dependencies = [ "clap", "stegano-core", @@ -793,13 +817,15 @@ dependencies = [ [[package]] name = "stegano-core" -version = "0.4.10" +version = "0.5.0" dependencies = [ "bitstream-io", "byteorder", "bzip2", + "cargo-husky", "criterion", "deflate 1.0.0", + "enum_dispatch", "hound", "image", "speculate", diff --git a/README.md b/README.md index c062487..9eef2e1 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,10 @@ Let's unveil the raw data of the `README.md` that we've hidden just above in `RE The file `README.bin` contains all raw binary data unfiltered decoded by the LSB decoding algorithm. That is for the curious people, and not so much interesting for regular usage. +## stegano on the web + +- [announcement on reddit](https://www.reddit.com/r/rust/comments/fbavos/command_line_steganography_for_png_images_written/) + ## Contribute To contribute to stegano-rs you can either checkout existing issues [labeled with `good first issue`][4] or [open a new issue][5] diff --git a/stegano-cli/Cargo.toml b/stegano-cli/Cargo.toml index 43329b7..b0ed433 100644 --- a/stegano-cli/Cargo.toml +++ b/stegano-cli/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "stegano-cli" description = "Hiding secret data with steganography in PNG images and WAV audio files" -version = "0.4.10" +version = "0.5.0" authors = ["Sven Assmann "] -edition = "2018" +edition = "2021" license = "GPL-3.0-only" homepage = "https://www.stegano.org" readme = "README.md" @@ -17,9 +17,9 @@ travis-ci = { repository = "steganogram/stegano-rs", branch = "main" } maintenance = { status = "passively-maintained" } [dependencies] -stegano-core = { path = "../stegano-core", version = "0.4.10" } -#stegano-core = "0.4.5" -clap = "2.33" +stegano-core = { path = "../stegano-core", version = "0.5.0" } +#stegano-core = "0.5.0" +clap = "2.34" [[bin]] name = "stegano" diff --git a/stegano-cli/src/main.rs b/stegano-cli/src/main.rs index a782e64..e19fbed 100644 --- a/stegano-cli/src/main.rs +++ b/stegano-cli/src/main.rs @@ -1,7 +1,10 @@ -use clap::{crate_authors, crate_description, crate_version, App, AppSettings, Arg, SubCommand}; +use clap::{ + crate_authors, crate_description, crate_version, App, AppSettings, Arg, ArgMatches, SubCommand, +}; use std::path::Path; use stegano_core::commands::{unveil, unveil_raw}; +use stegano_core::media::image::lsb_codec::CodecOptions; use stegano_core::*; fn main() -> Result<()> { @@ -99,11 +102,21 @@ fn main() -> Result<()> { .required(true) .help("Raw data will be stored as binary file"), ) - ).get_matches(); + ) + .arg( + Arg::with_name("color_step_increment") + .long("x-color-step-increment") + .value_name("color channel step increment") + .takes_value(true) + .default_value("1") + .required(false) + .help("Experimental: image color channel step increment"), + ) + .get_matches(); match matches.subcommand() { ("hide", Some(m)) => { - let mut s = SteganoCore::encoder(); + let mut s = SteganoCore::encoder_with_options(get_options(m)); s.use_media(m.value_of("media").unwrap())? .write_to(m.value_of("write_to_file").unwrap()); @@ -132,6 +145,7 @@ fn main() -> Result<()> { unveil( Path::new(m.value_of("input_image").unwrap()), Path::new(m.value_of("output_folder").unwrap()), + &get_options(m), )?; } ("unveil-raw", Some(m)) => { @@ -145,3 +159,15 @@ fn main() -> Result<()> { Ok(()) } + +fn get_options(args: &ArgMatches) -> CodecOptions { + let mut c = CodecOptions::default(); + if args.is_present("color_step_increment") { + c.color_channel_step_increment = args + .value_of("color_step_increment") + .unwrap() + .parse() + .unwrap(); + } + c +} diff --git a/stegano-core/Cargo.toml b/stegano-core/Cargo.toml index d8092ed..426a425 100644 --- a/stegano-core/Cargo.toml +++ b/stegano-core/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "stegano-core" description = "Steganography core library for stegano-cli. Supports hiding data in PNG images via LSB Encoding." -version = "0.4.10" +version = "0.5.0" authors = ["Sven Assmann "] -edition = "2018" +edition = "2021" license = "GPL-3.0-only" homepage = "https://www.stegano.org" readme = "README.md" @@ -26,19 +26,37 @@ deflate = "1.0" byteorder = "1.4" hound = "3.4" thiserror = "1.0" +enum_dispatch = "0.3" [dev-dependencies] speculate = "0.1" -criterion = "0.3" +criterion = { version = "0.3", features = ["html_reports"] } tempfile = "3.2" +[dev-dependencies.cargo-husky] +version = "1" +default-features = false +features = ["prepush-hook", "run-cargo-test", "run-cargo-clippy", "run-cargo-fmt"] + [lib] bench = false [[bench]] -name = "decoder_benchmark" +name = "image_decoding" +path = "benches/image/decoding.rs" +harness = false + +[[bench]] +name = "image_encoding" +path = "benches/image/encoding.rs" +harness = false + +[[bench]] +name = "audio_decoding" +path = "benches/audio/decoding.rs" harness = false [[bench]] -name = "encoder_benchmark" +name = "audio_encoding" +path = "benches/audio/encoding.rs" harness = false diff --git a/stegano-core/benches/audio/decoding.rs b/stegano-core/benches/audio/decoding.rs new file mode 100644 index 0000000..c4e90b5 --- /dev/null +++ b/stegano-core/benches/audio/decoding.rs @@ -0,0 +1,21 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use hound::WavReader; +use stegano_core::media::audio::LsbCodec; + +pub fn audio_decoding(c: &mut Criterion) { + c.bench_function("Audio Decoding", |b| { + let mut reader = WavReader::open("../resources/secrets/audio-with-secrets.wav") + .expect("Cannot create reader"); + let mut buf = [0; 12]; + + b.iter(|| { + reader.seek(0).expect("Cannot seek to 0"); + LsbCodec::decoder(&mut reader) + .read_exact(&mut buf) + .expect("Cannot read 12 bytes from decoder"); + }) + }); +} + +criterion_group!(benches, audio_decoding); +criterion_main!(benches); diff --git a/stegano-core/benches/audio/encoding.rs b/stegano-core/benches/audio/encoding.rs new file mode 100644 index 0000000..81c60f8 --- /dev/null +++ b/stegano-core/benches/audio/encoding.rs @@ -0,0 +1,21 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use hound::WavReader; +use stegano_core::media::audio::LsbCodec; + +pub fn audio_encoding(c: &mut Criterion) { + c.bench_function("Audio Encoding to memory", |b| { + let mut reader = + WavReader::open("../resources/plain/carrier-audio.wav").expect("Cannot create reader"); + let mut samples = reader.samples().map(|s| s.unwrap()).collect::>(); + let secret_message = b"Hello World!"; + + b.iter(|| { + LsbCodec::encoder(&mut samples) + .write_all(&secret_message[..]) + .expect("Cannot write to codec"); + }) + }); +} + +criterion_group!(benches, audio_encoding); +criterion_main!(benches); diff --git a/stegano-core/benches/decoder_benchmark.rs b/stegano-core/benches/decoder_benchmark.rs deleted file mode 100644 index 1d9ce4b..0000000 --- a/stegano-core/benches/decoder_benchmark.rs +++ /dev/null @@ -1,43 +0,0 @@ -use criterion::{criterion_group, criterion_main, Criterion}; -use std::io::Read; -use stegano_core::media::image::decoder::ImagePngSource; -use stegano_core::universal_decoder::{Decoder, OneBitUnveil}; - -pub fn stegano_image_benchmark(c: &mut Criterion) { - let img = image::open("../resources/with_text/hello_world.png") - .expect("Input image is not readable.") - .to_rgba8(); - - c.bench_function("SteganoCore Image Decoding", |b| { - b.iter(|| { - let mut buf = vec![0; 13]; - Decoder::new(ImagePngSource::new(&img), OneBitUnveil) - .read_exact(&mut buf) - .expect("Failed to read 13 bytes"); - let msg = String::from_utf8(buf).expect("Failed to convert result to string"); - assert_eq!("\u{1}Hello World!", msg) - }) - }); -} - -pub fn stegano_audio_benchmark(c: &mut Criterion) { - use hound::WavReader; - use stegano_core::media::audio::LsbCodec; - let mut reader = WavReader::open("../resources/secrets/audio-with-secrets.wav") - .expect("Cannot create reader"); - - c.bench_function("SteganoCore Audio Decoding", |b| { - b.iter(|| { - reader.seek(0).expect("Cannot seek to 0"); - let mut buf = vec![0; 12]; - LsbCodec::decoder(&mut reader) - .read_exact(&mut buf) - .expect("Cannot read 12 bytes from decoder"); - let msg = String::from_utf8(buf).expect("Cannot convert result to string"); - assert_eq!("Hello World!", msg); - }) - }); -} - -criterion_group!(benches, stegano_image_benchmark, stegano_audio_benchmark); -criterion_main!(benches); diff --git a/stegano-core/benches/encoder_benchmark.rs b/stegano-core/benches/encoder_benchmark.rs deleted file mode 100644 index 1420047..0000000 --- a/stegano-core/benches/encoder_benchmark.rs +++ /dev/null @@ -1,40 +0,0 @@ -use criterion::{criterion_group, criterion_main, Criterion}; -use stegano_core::media::image::LsbCodec; - -pub fn stegano_image_benchmark(c: &mut Criterion) { - let plain_image = image::open("../resources/plain/carrier-image.png") - .expect("Input image is not readable.") - .to_rgba8(); - let (width, height) = plain_image.dimensions(); - let secret_message = b"Hello World!"; - - c.bench_function("SteganoCore Image Encoding to memory", |b| { - b.iter(|| { - let mut image_with_secret = image::RgbaImage::new(width, height); - LsbCodec::encoder(&mut image_with_secret) - .write_all(&secret_message[..]) - .expect("Cannot write to codec"); - }) - }); -} - -pub fn stegano_audio_benchmark(c: &mut Criterion) { - use hound::WavReader; - use stegano_core::media::audio::LsbCodec; - - let mut reader = - WavReader::open("../resources/plain/carrier-audio.wav").expect("Cannot create reader"); - let mut samples = reader.samples().map(|s| s.unwrap()).collect::>(); - let secret_message = b"Hello World!"; - - c.bench_function("SteganoCore Audio Encoding to memory", |b| { - b.iter(|| { - LsbCodec::encoder(&mut samples) - .write_all(&secret_message[..]) - .expect("Cannot write to codec"); - }) - }); -} - -criterion_group!(benches, stegano_image_benchmark, stegano_audio_benchmark); -criterion_main!(benches); diff --git a/stegano-core/benches/image/decoding.rs b/stegano-core/benches/image/decoding.rs new file mode 100644 index 0000000..f7f74ca --- /dev/null +++ b/stegano-core/benches/image/decoding.rs @@ -0,0 +1,22 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use std::io::Read; +use stegano_core::media::image::decoder::ImageRgbaColor; +use stegano_core::universal_decoder::{Decoder, OneBitUnveil}; + +pub fn image_decoding(c: &mut Criterion) { + c.bench_function("Image Decoding", |b| { + let img = image::open("../resources/with_text/hello_world.png") + .expect("Input image is not readable.") + .to_rgba8(); + let mut buf = [0; 13]; + + b.iter(|| { + Decoder::new(ImageRgbaColor::new(&img), OneBitUnveil) + .read_exact(&mut buf) + .expect("Failed to read 13 bytes"); + }) + }); +} + +criterion_group!(benches, image_decoding); +criterion_main!(benches); diff --git a/stegano-core/benches/image/encoding.rs b/stegano-core/benches/image/encoding.rs new file mode 100644 index 0000000..b601067 --- /dev/null +++ b/stegano-core/benches/image/encoding.rs @@ -0,0 +1,22 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use std::io::Write; +use stegano_core::media::image::encoder::ImageRgbaColorMut; +use stegano_core::universal_encoder::{Encoder, OneBitHide}; + +pub fn image_encoding(c: &mut Criterion) { + c.bench_function("Image Encoding", |b| { + let mut plain_image = image::open("../resources/plain/carrier-image.png") + .expect("Input image is not readable.") + .to_rgba8(); + let secret_message = b"Hello World!"; + + b.iter(|| { + Encoder::new(ImageRgbaColorMut::new(&mut plain_image), OneBitHide) + .write_all(&secret_message[..]) + .expect("Cannot write secret message"); + }) + }); +} + +criterion_group!(benches, image_encoding); +criterion_main!(benches); diff --git a/stegano-core/src/commands.rs b/stegano-core/src/commands.rs index 7860286..4d0098a 100644 --- a/stegano-core/src/commands.rs +++ b/stegano-core/src/commands.rs @@ -1,17 +1,21 @@ use crate::media::audio::wav_iter::AudioWavIter; use crate::media::image::LsbCodec; use crate::universal_decoder::{Decoder, OneBitUnveil}; -use crate::{Media, Message, RawMessage, SteganoError}; +use crate::{CodecOptions, Media, Message, RawMessage, SteganoError}; use std::fs::File; use std::io::Write; use std::path::Path; -pub fn unveil(secret_media: &Path, destination: &Path) -> Result<(), SteganoError> { +pub fn unveil( + secret_media: &Path, + destination: &Path, + opts: &CodecOptions, +) -> Result<(), SteganoError> { let media = Media::from_file(secret_media)?; let files = match media { Media::Image(image) => { - let mut decoder = LsbCodec::decoder(&image); + let mut decoder = LsbCodec::decoder(&image, opts); let msg = Message::of(&mut decoder); let mut files = msg.files; @@ -63,7 +67,7 @@ pub fn unveil_raw(secret_media: &Path, destination_file: &Path) -> Result<(), St match media { Media::Image(image) => { - let mut decoder = LsbCodec::decoder(&image); + let mut decoder = LsbCodec::decoder(&image, &CodecOptions::default()); let msg = RawMessage::of(&mut decoder); let mut destination_file = File::create(destination_file) .map_err(|source| SteganoError::WriteError { source })?; diff --git a/stegano-core/src/encoder.rs b/stegano-core/src/encoder.rs deleted file mode 100644 index 08dc91c..0000000 --- a/stegano-core/src/encoder.rs +++ /dev/null @@ -1,19 +0,0 @@ -/// generic stegano encoder -pub(crate) struct Encoder<'i, I, O, A, P> { - pub(crate) input: &'i mut I, - pub(crate) output: &'i mut O, - pub(crate) algorithm: A, - pub(crate) position: P, -} - -/// generic stegano encoder constructor method -impl<'i, I, O, A, P> Encoder<'i, I, O, A, P> { - pub fn new(input: &'i mut I, output: &'i mut O, algorithm: A, position: P) -> Self { - Encoder { - input, - output, - algorithm, - position, - } - } -} diff --git a/stegano-core/src/lib.rs b/stegano-core/src/lib.rs index 429f431..787523e 100644 --- a/stegano-core/src/lib.rs +++ b/stegano-core/src/lib.rs @@ -22,7 +22,7 @@ //! ## Unveil data from an image //! //! ```rust -//! use stegano_core::{SteganoCore, SteganoEncoder}; +//! use stegano_core::{SteganoCore, SteganoEncoder, CodecOptions}; //! use stegano_core::commands::unveil; //! use std::path::Path; //! @@ -34,7 +34,8 @@ //! //! unveil( //! &Path::new("image-with-a-file-inside.png"), -//! &Path::new("./")); +//! &Path::new("./"), +//! &CodecOptions::default()); //! ``` //! //! [core]: ./struct.SteganoCore.html @@ -42,11 +43,39 @@ //! [dec]: ./struct.SteganoDecoder.html //! [raw]: ./struct.SteganoRawDecoder.html +#![warn( +// clippy::cargo_common_metadata, +// clippy::branches_sharing_code, +// clippy::cast_lossless, +// clippy::cognitive_complexity, +// clippy::get_unwrap, +// clippy::if_then_some_else_none, +// clippy::inefficient_to_string, +// clippy::match_bool, +// clippy::missing_const_for_fn, +// clippy::missing_panics_doc, +// clippy::option_if_let_else, +// clippy::redundant_closure, +clippy::redundant_else, +// clippy::redundant_pub_crate, +// clippy::ref_binding_to_reference, +// clippy::ref_option_ref, +// clippy::same_functions_in_if_condition, +// clippy::unneeded_field_pattern, +// clippy::unnested_or_patterns, +// clippy::use_self, +)] + pub mod bit_iterator; + pub use bit_iterator::BitIterator; + pub mod message; + pub use message::*; + pub mod raw_message; + pub use raw_message::*; pub mod commands; @@ -56,10 +85,13 @@ pub mod universal_encoder; use hound::{WavReader, WavSpec, WavWriter}; use image::RgbaImage; +use std::default::Default; use std::fs::File; use std::path::Path; use thiserror::Error; +pub use crate::media::image::CodecOptions; + #[derive(Error, Debug)] pub enum SteganoError { /// Represents an unsupported carrier media. For example, a Movie file is not supported @@ -147,19 +179,24 @@ pub enum Media { } pub struct SteganoCore {} + impl SteganoCore { pub fn encoder() -> SteganoEncoder { - SteganoEncoder::new() + SteganoEncoder::with_options(CodecOptions::default()) + } + + pub fn encoder_with_options(opts: CodecOptions) -> SteganoEncoder { + SteganoEncoder::with_options(opts) } } pub trait Hide { fn hide_message(&mut self, message: &Message) -> Result<&mut Media>; -} - -pub trait Unveil { - // TODO should return Result<()> - fn unveil(&mut self) -> &mut Self; + fn hide_message_with_options( + &mut self, + message: &Message, + opts: &CodecOptions, + ) -> Result<&mut Media>; } impl Media { @@ -220,13 +257,21 @@ impl Persist for Media { impl Hide for Media { fn hide_message(&mut self, message: &Message) -> Result<&mut Self> { + self.hide_message_with_options(message, &CodecOptions::default()) + } + + fn hide_message_with_options( + &mut self, + message: &Message, + opts: &CodecOptions, + ) -> Result<&mut Media> { let buf: Vec = message.into(); match self { Media::Image(i) => { let (width, height) = i.dimensions(); let _space_to_fill = (width * height * 3) / 8; - let mut encoder = media::image::LsbCodec::encoder(i); + let mut encoder = media::image::LsbCodec::encoder(i, opts); encoder .write_all(buf.as_ref()) @@ -246,6 +291,7 @@ impl Hide for Media { } pub struct SteganoEncoder { + options: CodecOptions, target: Option, carrier: Option, message: Message, @@ -254,6 +300,7 @@ pub struct SteganoEncoder { impl Default for SteganoEncoder { fn default() -> Self { Self { + options: CodecOptions::default(), target: None, carrier: None, message: Message::empty(), @@ -265,6 +312,12 @@ impl SteganoEncoder { pub fn new() -> Self { Self::default() } + pub fn with_options(opts: CodecOptions) -> Self { + Self { + options: opts, + ..Self::default() + } + } pub fn use_media(&mut self, input_file: &str) -> Result<&mut Self> { let path = Path::new(input_file); @@ -324,7 +377,8 @@ impl SteganoEncoder { if let Some(media) = self.carrier.as_mut() { media - .hide_message(&self.message) + // .hide_message(&self.message) + .hide_message_with_options(&self.message, &self.options) .expect("Failed to hide message in media") .save_as(Path::new(self.target.as_ref().unwrap())) .expect("Failed to save media"); @@ -410,7 +464,11 @@ mod e2e_tests { .len(); assert!(l > 0, "File is not supposed to be empty"); - unveil(secret_media_p.as_path(), out_dir.path())?; + unveil( + secret_media_p.as_path(), + out_dir.path(), + &CodecOptions::default(), + )?; let given_decoded_secret = out_dir.path().join("Cargo.toml"); assert_eq_file_content( @@ -439,7 +497,11 @@ mod e2e_tests { .len(); assert!(l > 0, "File is not supposed to be empty"); - unveil(image_with_secret_path.as_path(), out_dir.path())?; + unveil( + image_with_secret_path.as_path(), + out_dir.path(), + &CodecOptions::default(), + )?; let given_decoded_secret = out_dir.path().join("Cargo.toml"); assert_eq_file_content( @@ -491,7 +553,11 @@ mod e2e_tests { .len(); assert!(l > 0, "File is not supposed to be empty"); - unveil(image_with_secret_path.as_path(), out_dir.path())?; + unveil( + image_with_secret_path.as_path(), + out_dir.path(), + &CodecOptions::default(), + )?; assert_eq_file_content( &expected_file, secret_to_hide.as_ref(), @@ -517,7 +583,11 @@ mod e2e_tests { assert_file_not_empty(image_with_secret); - unveil(image_with_secret_path.as_path(), out_dir.path())?; + unveil( + image_with_secret_path.as_path(), + out_dir.path(), + &CodecOptions::default(), + )?; assert_eq_file_content( &expected_file, @@ -536,6 +606,7 @@ mod e2e_tests { unveil( Path::new("../resources/with_attachment/Blah.txt.png"), out_dir.path(), + &CodecOptions::default(), )?; assert_eq_file_content( @@ -556,6 +627,7 @@ mod e2e_tests { unveil( Path::new("../resources/with_attachment/Blah.txt__and__Blah-2.txt.png"), out_dir.path(), + &CodecOptions::default(), )?; assert_eq_file_content( &decoded_secret_1, @@ -588,7 +660,11 @@ mod e2e_tests { assert_file_not_empty(image_with_secret); - unveil(image_with_secret_path.as_path(), out_dir.path())?; + unveil( + image_with_secret_path.as_path(), + out_dir.path(), + &CodecOptions::default(), + )?; let decoded_secret = out_dir.path().join("Blah.txt"); assert_eq_file_content( @@ -625,3 +701,17 @@ mod e2e_tests { assert!(l > 0, "File is not supposed to be empty"); } } + +#[cfg(test)] +mod test_utils { + use image::{ImageBuffer, RgbaImage}; + + pub const HELLO_WORLD_PNG: &str = "../resources/with_text/hello_world.png"; + + pub fn prepare_small_image() -> RgbaImage { + ImageBuffer::from_fn(5, 5, |x, y| { + let i = (4 * x + 20 * y) as u8; + image::Rgba([i, i + 1, i + 2, i + 3]) + }) + } +} diff --git a/stegano-core/src/media/audio/lsb_codec.rs b/stegano-core/src/media/audio/lsb_codec.rs index 3dc6507..d4432a8 100644 --- a/stegano-core/src/media/audio/lsb_codec.rs +++ b/stegano-core/src/media/audio/lsb_codec.rs @@ -5,7 +5,7 @@ use hound::{WavReader, WavSpec}; use crate::media::audio::wav_iter::{AudioWavIter, AudioWavIterMut}; use crate::universal_decoder::{Decoder, OneBitUnveil}; -use crate::universal_encoder::Encoder; +use crate::universal_encoder::{Encoder, OneBitHide}; /// convenient wrapper for `WavReader::open` pub fn read_samples(file: &Path) -> (Vec, WavSpec) { @@ -69,7 +69,10 @@ impl LsbCodec { /// .expect("Cannot write to codec"); /// ``` pub fn encoder<'i>(input: &'i mut Vec) -> Box { - Box::new(Encoder::new(AudioWavIterMut::new(input.iter_mut()))) + Box::new(Encoder::new( + AudioWavIterMut::new(input.iter_mut()), + OneBitHide, + )) } } diff --git a/stegano-core/src/media/audio/wav_iter.rs b/stegano-core/src/media/audio/wav_iter.rs index 375d4fd..b3051a0 100644 --- a/stegano-core/src/media/audio/wav_iter.rs +++ b/stegano-core/src/media/audio/wav_iter.rs @@ -91,8 +91,6 @@ impl<'a> Iterator for AudioWavIterMut<'a, i16> { type Item = MediaPrimitiveMut<'a>; fn next(&mut self) -> Option { - self.samples - .next() - .map(|s| MediaPrimitiveMut::AudioSample(s)) + self.samples.next().map(MediaPrimitiveMut::AudioSample) } } diff --git a/stegano-core/src/media/image/decoder.rs b/stegano-core/src/media/image/decoder.rs index c68e275..6147049 100644 --- a/stegano-core/src/media/image/decoder.rs +++ b/stegano-core/src/media/image/decoder.rs @@ -1,5 +1,7 @@ +use crate::media::image::iterators::{ColorIter, Transpose}; +use crate::media::image::lsb_codec::CodecOptions; use crate::MediaPrimitive; -use image::RgbaImage; +use image::{Rgba, RgbaImage}; /// stegano source for image files, based on `RgbaImage` by `image` crate /// @@ -9,7 +11,7 @@ use image::RgbaImage; /// use std::io::Read; /// use image::{RgbaImage}; /// use stegano_core::universal_decoder::{Decoder, OneBitUnveil}; -/// use stegano_core::media::image::decoder::ImagePngSource; +/// use stegano_core::media::image::decoder::ImageRgbaColor; /// /// // create a `RgbaImage` from a png image file /// let mut image = image::open("../resources/with_text/hello_world.png") @@ -18,60 +20,60 @@ use image::RgbaImage; /// let mut secret = vec![0; 13]; /// /// // create a `Decoder` based on an `ImagePngSource` based on the `RgbaImage` -/// Decoder::new(ImagePngSource::new(&mut image), OneBitUnveil) +/// Decoder::new(ImageRgbaColor::new(&mut image), OneBitUnveil) /// .read_exact(&mut secret) /// .expect("Cannot read 13 bytes from decoder"); /// /// let msg = String::from_utf8(secret).expect("Cannot convert result to string"); /// assert_eq!("\u{1}Hello World!", msg); /// ``` -pub struct ImagePngSource<'i> { - pub input: &'i RgbaImage, - max_x: u32, - max_y: u32, - max_c: u8, - pub x: u32, - pub y: u32, - pub c: u8, +pub struct ImageRgbaColor<'i> { + i: usize, + steps: usize, + skip_alpha: bool, + pixel: ColorIter<'i, Rgba>, } -impl<'i> ImagePngSource<'i> { +impl<'i> ImageRgbaColor<'i> { /// constructor for a given `RgbaImage` that lives somewhere pub fn new(input: &'i RgbaImage) -> Self { - let (max_x, max_y) = input.dimensions(); + Self::new_with_options(input, &CodecOptions::default()) + } + + pub fn new_with_options(input: &'i RgbaImage, options: &CodecOptions) -> Self { + let h = input.height(); Self { - input, - max_x, - max_y, - max_c: 3, - x: 0, - y: 0, - c: 0, + i: 0, + steps: options.get_color_channel_step_increment(), + skip_alpha: options.get_skip_alpha_channel(), + pixel: ColorIter::from_transpose(Transpose::from_rows(input.rows(), h)), } } } /// iterates over the image and returns single color channels of each pixel wrapped into a `CarrierItem` -impl<'i> Iterator for ImagePngSource<'i> { +impl<'i> Iterator for ImageRgbaColor<'i> { type Item = MediaPrimitive; #[inline(always)] - fn next(&mut self) -> Option { - if self.x == self.max_x { - return None; - } - let pixel = self.input.get_pixel(self.x, self.y); - let result = Some(MediaPrimitive::ImageColorChannel(pixel.0[self.c as usize])); - self.c += 1; - if self.c == self.max_c { - self.c = 0; - self.y += 1; + fn next(&'_ mut self) -> Option { + if self.skip_alpha && self.i > 0 { + let is_next_alpha = (self.i + 1) % 4 == 0; + if is_next_alpha { + self.pixel.next(); + self.i += 1; + } } - if self.y == self.max_y { - self.y = 0; - self.x += 1; + let res = self + .pixel + .next() + .map(|c| MediaPrimitive::ImageColorChannel(*c)); + self.i += 1; + for _ in 0..self.steps - 1 { + self.pixel.next(); + self.i += 1; } - result + res } } @@ -90,7 +92,7 @@ mod decoder_tests { let first_pixel = *img.get_pixel(0, 0); let second_pixel = *img.get_pixel(0, 1); let second_row_first_pixel = *img.get_pixel(1, 0); - let mut source = ImagePngSource::new(&img); + let mut source = ImageRgbaColor::new(&img); assert_eq!( source.next().unwrap(), MediaPrimitive::ImageColorChannel(first_pixel.0[0]), @@ -120,7 +122,7 @@ mod decoder_tests { .expect("Input image is not readable.") .to_rgba8(); let (width, height) = img.dimensions(); - let mut source = ImagePngSource::new(&img); + let mut source = ImageRgbaColor::new(&img); assert_ne!( source.nth(((height * width * 3) - 1) as usize), None, diff --git a/stegano-core/src/media/image/encoder.rs b/stegano-core/src/media/image/encoder.rs index aa45caa..f0f5fbd 100644 --- a/stegano-core/src/media/image/encoder.rs +++ b/stegano-core/src/media/image/encoder.rs @@ -1,6 +1,7 @@ use image::{Rgba, RgbaImage}; -use crate::media::image::iterators::{ColorIter, TransposeMut}; +use crate::media::image::iterators::{ColorIterMut, TransposeMut}; +use crate::media::image::lsb_codec::CodecOptions; use crate::MediaPrimitiveMut; /// stegano source for image files, based on `RgbaImage` by `image` crate @@ -11,8 +12,8 @@ use crate::MediaPrimitiveMut; /// use std::io::{Read, Write}; /// use image::{RgbaImage}; /// use stegano_core::universal_decoder::{Decoder, OneBitUnveil}; -/// use stegano_core::media::image::encoder::ImagePngMut; -/// use stegano_core::universal_encoder::Encoder; +/// use stegano_core::media::image::encoder::ImageRgbaColorMut; +/// use stegano_core::universal_encoder::{Encoder, OneBitHide}; /// /// // create a `RgbaImage` from a png image file /// let image_original = image::open("../resources/plain/carrier-image.png") @@ -23,47 +24,118 @@ use crate::MediaPrimitiveMut; /// .to_rgba8(); /// let secret_message = "Hello World!".as_bytes(); /// { -/// let mut encoder = Encoder::new(ImagePngMut::new(&mut image).into_iter()); +/// let mut encoder = Encoder::new(ImageRgbaColorMut::new(&mut image).into_iter(), OneBitHide); /// encoder.write_all(secret_message) /// .expect("Cannot write secret message"); /// } /// assert_ne!(image_original.get_pixel(0, 0), image.get_pixel(0, 0)); /// ``` -pub struct ImagePngMut<'a> { +pub struct ImageRgbaColorMut<'a> { i: usize, - pixel: ColorIter<'a, Rgba>, + steps: usize, + skip_alpha: bool, + pixel: ColorIterMut<'a, Rgba>, } -impl<'a> ImagePngMut<'a> { +impl<'a> ImageRgbaColorMut<'a> { /// constructor for a given `RgbaImage` that lives somewhere pub fn new(input: &'a mut RgbaImage) -> Self { + Self::new_with_options(input, &CodecOptions::default()) + } + + pub fn new_with_options(input: &'a mut RgbaImage, options: &CodecOptions) -> Self { let h = input.height(); Self { i: 0, - pixel: ColorIter::from_transpose(TransposeMut::from_rows_mut(input.rows_mut(), h)), + steps: options.get_color_channel_step_increment(), + skip_alpha: options.get_skip_alpha_channel(), + pixel: ColorIterMut::from_transpose(TransposeMut::from_rows_mut(input.rows_mut(), h)), } } } -impl<'i> Iterator for ImagePngMut<'i> { +impl<'i> Iterator for ImageRgbaColorMut<'i> { type Item = MediaPrimitiveMut<'i>; fn next(&'_ mut self) -> Option { - let is_alpha = (self.i + 1) % 4 == 0; - if is_alpha { + if self.skip_alpha && self.i > 0 { + let is_next_alpha = (self.i + 1) % 4 == 0; + if is_next_alpha { + self.pixel.next(); + self.i += 1; + } + } + let res = self.pixel.next().map(MediaPrimitiveMut::ImageColorChannel); + self.i += 1; + for _ in 0..self.steps - 1 { self.pixel.next(); self.i += 1; } - self.i += 1; - self.pixel.next().map(MediaPrimitiveMut::ImageColorChannel) + res } } #[cfg(test)] mod decoder_tests { use super::*; + use crate::media::image::lsb_codec::Concealer; + use crate::test_utils::{prepare_small_image, HELLO_WORLD_PNG}; + + #[test] + fn it_should_step_in_increments_smaller_than_one_pixel() { + let img_ro = prepare_small_image(); + let mut img = img_ro.clone(); + let mut carrier = ImageRgbaColorMut::new_with_options( + &mut img, + &CodecOptions { + skip_alpha_channel: true, + color_channel_step_increment: 2, + concealer: Concealer::LeastSignificantBit, + }, + ); + + if let Some(MediaPrimitiveMut::ImageColorChannel(b)) = carrier.nth(1) { + let (x, y, c) = (0, 0, 2); + let pixel = img_ro.get_pixel(x, y); + let expected_color = *pixel.0.get(c).unwrap(); + + let actual_color = *b; - const HELLO_WORLD_PNG: &str = "../resources/with_text/hello_world.png"; + assert_eq!( + expected_color, actual_color, + "Pixel at (x={}, y={}) @ color {} mismatched expected={:?}", + x, y, c, pixel.0 + ); + } + } + + #[test] + fn it_should_step_in_increments_bigger_than_one_pixel() { + let img_ro = prepare_small_image(); + let mut img = img_ro.clone(); + let mut carrier = ImageRgbaColorMut::new_with_options( + &mut img, + &CodecOptions { + skip_alpha_channel: true, + color_channel_step_increment: 3, + concealer: Concealer::LeastSignificantBit, + }, + ); + + if let Some(MediaPrimitiveMut::ImageColorChannel(b)) = carrier.nth(1) { + let (x, y, c) = (0, 1, 0); + let pixel = img_ro.get_pixel(x, y); + let expected_color = *pixel.0.get(c).unwrap(); + + let actual_color = *b; + + assert_eq!( + expected_color, actual_color, + "Pixel at (x={}, y={}) @ color {} mismatched expected={:?}", + x, y, c, pixel.0 + ); + } + } #[test] fn it_should_iterate_columns_first_and_only_3_color_channels() { @@ -74,7 +146,7 @@ mod decoder_tests { .expect("Input image is not readable.") .to_rgba8(); let (width, height) = img.dimensions(); - let mut carrier = ImagePngMut::new(&mut img); + let mut carrier = ImageRgbaColorMut::new(&mut img); for x in 0..width { for y in 0..height { @@ -102,7 +174,7 @@ mod decoder_tests { .to_rgba8(); let first_pixel = *img.get_pixel(0, 0); { - let mut carrier = ImagePngMut::new(&mut img); + let mut carrier = ImageRgbaColorMut::new(&mut img); if let MediaPrimitiveMut::ImageColorChannel(color) = carrier.next().unwrap() { *color += 0x2; } diff --git a/stegano-core/src/media/image/iterators.rs b/stegano-core/src/media/image/iterators.rs index 9468759..cbc3953 100644 --- a/stegano-core/src/media/image/iterators.rs +++ b/stegano-core/src/media/image/iterators.rs @@ -1,6 +1,6 @@ -use image::buffer::{PixelsMut, RowsMut}; +use image::buffer::{Pixels, PixelsMut, Rows, RowsMut}; use image::Pixel; -use std::slice::IterMut; +use std::slice::{Iter, IterMut}; /// Allows transposed mutable access to pixel, like column based pub(crate) struct TransposeMut<'a, P: Pixel + 'a> { @@ -42,12 +42,51 @@ impl<'a, P: Pixel + 'a> Iterator for TransposeMut<'a, P> { } } -pub(crate) struct ColorIter<'a, P: Pixel + 'a> { +pub(crate) struct Transpose<'a, P: Pixel + 'a> { + i: usize, + height: u32, + rows: Rows<'a, P>, + rows_buffer: Vec>, +} + +impl<'a, P: Pixel + 'a> Transpose<'a, P> { + /// utilizes Rows to give column based readonly access to pixel + pub fn from_rows(rows: Rows<'a, P>, height: u32) -> Self { + Self { + i: 0, + height, + rows, + rows_buffer: Vec::with_capacity(height as usize), + } + } +} + +impl<'a, P: Pixel + 'a> Iterator for Transpose<'a, P> { + type Item = &'a P; + + fn next(&mut self) -> Option { + let row_idx = ((self.i as u32) % self.height) as usize; + self.i += 1; + match self.rows_buffer.get_mut(row_idx) { + None => match self.rows.next() { + Some(mut row) => { + let p = row.next(); + self.rows_buffer.push(row); + p + } + _ => None, + }, + Some(row) => row.next(), + } + } +} + +pub(crate) struct ColorIterMut<'a, P: Pixel + 'a> { pixel: TransposeMut<'a, P>, colors: IterMut<'a, P::Subpixel>, } -impl<'a, P: Pixel + 'a> ColorIter<'a, P> { +impl<'a, P: Pixel + 'a> ColorIterMut<'a, P> { pub fn from_transpose(mut t: TransposeMut<'a, P>) -> Self { let i = t.next().unwrap().channels_mut().iter_mut(); Self { @@ -57,12 +96,42 @@ impl<'a, P: Pixel + 'a> ColorIter<'a, P> { } } -impl<'a, P: Pixel + 'a> Iterator for ColorIter<'a, P> { +impl<'a, P: Pixel + 'a> Iterator for ColorIterMut<'a, P> { type Item = &'a mut P::Subpixel; fn next(&mut self) -> Option { self.colors.next().or_else(|| { - self.colors = self.pixel.next().unwrap().channels_mut().iter_mut(); + if let Some(iter) = self.pixel.next() { + self.colors = iter.channels_mut().iter_mut(); + } + self.colors.next() + }) + } +} + +pub(crate) struct ColorIter<'a, P: Pixel + 'a> { + pixel: Transpose<'a, P>, + colors: Iter<'a, P::Subpixel>, +} + +impl<'a, P: Pixel + 'a> ColorIter<'a, P> { + pub fn from_transpose(mut t: Transpose<'a, P>) -> Self { + let i = t.next().unwrap().channels().iter(); + Self { + pixel: t, + colors: i, + } + } +} + +impl<'a, P: Pixel + 'a> Iterator for ColorIter<'a, P> { + type Item = &'a P::Subpixel; + + fn next(&mut self) -> Option { + self.colors.next().or_else(|| { + if let Some(iter) = self.pixel.next() { + self.colors = iter.channels().iter(); + } self.colors.next() }) } @@ -71,10 +140,9 @@ impl<'a, P: Pixel + 'a> Iterator for ColorIter<'a, P> { #[cfg(test)] mod tests { use super::*; + use crate::test_utils::HELLO_WORLD_PNG; use image::Rgba; - const HELLO_WORLD_PNG: &str = "../resources/with_text/hello_world.png"; - #[test] fn transpose_mut() { let mut img = image::open(HELLO_WORLD_PNG) @@ -115,7 +183,7 @@ mod tests { .to_rgba8(); let (width, height) = img.dimensions(); let mut c_iter = - ColorIter::from_transpose(TransposeMut::from_rows_mut(img.rows_mut(), height)); + ColorIterMut::from_transpose(TransposeMut::from_rows_mut(img.rows_mut(), height)); for x in 0..width { for y in 0..height { @@ -143,4 +211,35 @@ mod tests { // ); // } } + + #[cfg(test)] + mod color_iter { + use crate::media::image::iterators::*; + use crate::test_utils::prepare_small_image; + use image::Rgba; + + #[test] + fn should_transpose_read() { + let img = prepare_small_image(); + let mut iter = Transpose::from_rows(img.rows(), img.height()); + + assert_eq!(iter.next(), Some(&Rgba([0_u8, 1, 2, 3]))); + assert_eq!(iter.next(), Some(&Rgba([20_u8, 21, 22, 23]))); + assert_eq!(iter.next(), Some(&Rgba([40_u8, 41, 42, 43]))); + } + + #[test] + fn should_read_color() { + let img = prepare_small_image(); + let iter = Transpose::from_rows(img.rows(), img.height()); + let mut color_iter = ColorIter::from_transpose(iter); + + assert_eq!(color_iter.next(), Some(&0_u8)); + assert_eq!(color_iter.next(), Some(&1_u8)); + assert_eq!(color_iter.next(), Some(&2_u8)); + assert_eq!(color_iter.next(), Some(&3_u8)); + assert_eq!(color_iter.next(), Some(&20_u8)); + assert_eq!(color_iter.next(), Some(&21_u8)); + } + } } diff --git a/stegano-core/src/media/image/lsb_codec.rs b/stegano-core/src/media/image/lsb_codec.rs index 2eadfee..34160fe 100644 --- a/stegano-core/src/media/image/lsb_codec.rs +++ b/stegano-core/src/media/image/lsb_codec.rs @@ -1,10 +1,47 @@ -use crate::media::image::decoder::ImagePngSource; -use crate::media::image::encoder::ImagePngMut; +use crate::media::image::decoder::ImageRgbaColor; +use crate::media::image::encoder::ImageRgbaColorMut; use crate::universal_decoder::{Decoder, OneBitUnveil}; -use crate::universal_encoder::Encoder; +use crate::universal_encoder::{Encoder, HideAlgorithms, OneBitHide, OneBitInLowFrequencyHide}; use image::RgbaImage; use std::io::{Read, Write}; +#[derive(Debug)] +pub struct CodecOptions { + /// would move the by step n each iteration, + /// Note: the alpha channel is count as regular channel + pub color_channel_step_increment: usize, + /// if true no alpha channel would be used for encoding + pub skip_alpha_channel: bool, + /// the concealer strategy + pub concealer: Concealer, +} + +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub enum Concealer { + LeastSignificantBit, + LowFrequencies, +} + +impl Default for CodecOptions { + fn default() -> Self { + Self { + color_channel_step_increment: 1, + skip_alpha_channel: true, + concealer: Concealer::LeastSignificantBit, + } + } +} + +impl CodecOptions { + pub fn get_color_channel_step_increment(&self) -> usize { + self.color_channel_step_increment + } + + pub fn get_skip_alpha_channel(&self) -> bool { + self.skip_alpha_channel + } +} + /// Factory for decoder and encoder pub struct LsbCodec; @@ -13,7 +50,7 @@ impl LsbCodec { /// /// ## Example how to retrieve a decoder: /// ```rust - /// use stegano_core::media::image::LsbCodec; + /// use stegano_core::media::image::{CodecOptions, LsbCodec}; /// use image::RgbaImage; /// /// let mut image_with_secret = image::open("../resources/secrets/image-with-hello-world.png") @@ -21,22 +58,29 @@ impl LsbCodec { /// .to_rgba8(); /// /// let mut buf = vec![0; 13]; - /// LsbCodec::decoder(&mut image_with_secret) + /// LsbCodec::decoder(&mut image_with_secret, &CodecOptions::default()) /// .read_exact(&mut buf[..]) /// .expect("Cannot read 13 bytes from codec"); /// /// let msg = String::from_utf8(buf).expect("Cannot convert result to string"); /// assert_eq!(msg, "\u{1}Hello World!"); /// ``` - pub fn decoder<'i>(input: &'i RgbaImage) -> Box { - Box::new(Decoder::new(ImagePngSource::new(input), OneBitUnveil)) + pub fn decoder<'i>(input: &'i RgbaImage, opts: &CodecOptions) -> Box { + Box::new(Decoder::new( + ImageRgbaColor::new_with_options(input, opts), + match opts.concealer { + Concealer::LeastSignificantBit => OneBitUnveil, + Concealer::LowFrequencies => OneBitUnveil, + }, + )) } /// builds a LSB Image Encoder that implements Write /// ## Example how to retrieve an encoder: /// /// ```rust - /// use stegano_core::media::image::LsbCodec; + /// use stegano_core::media::image::{LsbCodec}; + /// use stegano_core::media::image::CodecOptions; /// use image::{RgbaImage, open}; /// /// let mut plain_image = open("../resources/plain/carrier-image.png") @@ -46,19 +90,26 @@ impl LsbCodec { /// let secret_message = "Hello World!".as_bytes(); /// /// { - /// LsbCodec::encoder(&mut plain_image) + /// LsbCodec::encoder(&mut plain_image, &CodecOptions::default()) /// .write_all(&secret_message[..]) /// .expect("Cannot write to codec"); /// } /// let mut buf = vec![0; secret_message.len()]; - /// LsbCodec::decoder(&mut plain_image.into()) + /// LsbCodec::decoder(&mut plain_image.into(), &CodecOptions::default()) /// .read_exact(&mut buf[..]) /// .expect("Cannot read 12 bytes from codec"); /// /// let msg = String::from_utf8(buf).expect("Cannot convert result to string"); /// assert_eq!(msg, "Hello World!"); /// ``` - pub fn encoder<'i>(carrier: &'i mut RgbaImage) -> Box { - Box::new(Encoder::new(ImagePngMut::new(carrier))) + pub fn encoder<'i>(carrier: &'i mut RgbaImage, opts: &CodecOptions) -> Box { + let algorithm: HideAlgorithms = match opts.concealer { + Concealer::LeastSignificantBit => OneBitHide.into(), + Concealer::LowFrequencies => OneBitInLowFrequencyHide.into(), + }; + Box::new(Encoder::new( + ImageRgbaColorMut::new_with_options(carrier, opts), + algorithm, + )) } } diff --git a/stegano-core/src/media/image/mod.rs b/stegano-core/src/media/image/mod.rs index c2e95bb..10eb25f 100644 --- a/stegano-core/src/media/image/mod.rs +++ b/stegano-core/src/media/image/mod.rs @@ -3,4 +3,4 @@ pub mod encoder; mod iterators; pub mod lsb_codec; -pub use lsb_codec::LsbCodec; +pub use lsb_codec::{CodecOptions, LsbCodec}; diff --git a/stegano-core/src/message.rs b/stegano-core/src/message.rs index 0841ce8..d746c87 100644 --- a/stegano-core/src/message.rs +++ b/stegano-core/src/message.rs @@ -48,7 +48,9 @@ impl Message { ContentVersion::V1 => Self::new_of_v1(dec), ContentVersion::V2 => Self::new_of_v2(dec), ContentVersion::V4 => Self::new_of_v4(dec), - ContentVersion::Unsupported(_) => panic!("Seems like not a valid stegano file."), + ContentVersion::Unsupported(_) => { + panic!("Seems like you've got an invalid stegano file") + } } } @@ -123,9 +125,8 @@ impl Message { if *b == EOF { eof = i; break; - } else { - eof = 0; } + eof = 0; } if eof > 0 { diff --git a/stegano-core/src/universal_decoder.rs b/stegano-core/src/universal_decoder.rs index 6a76189..6a1fcb7 100644 --- a/stegano-core/src/universal_decoder.rs +++ b/stegano-core/src/universal_decoder.rs @@ -1,18 +1,25 @@ use bitstream_io::{BitWrite, BitWriter, LittleEndian}; +use enum_dispatch::enum_dispatch; use std::io::{BufWriter, Read, Result}; use crate::MediaPrimitive; +#[enum_dispatch] +pub enum UnveilAlgorithms { + OneBitUnveil, +} + /// generic unveil algorithm -pub trait UnveilAlgorithm { - fn decode(&self, carrier: T) -> bool; +#[enum_dispatch(UnveilAlgorithms)] +pub trait UnveilAlgorithm { + fn decode(&self, carrier: MediaPrimitive) -> bool; } /// generic stegano decoder pub struct Decoder where I: Iterator, - A: UnveilAlgorithm, + A: UnveilAlgorithm, { pub input: I, pub algorithm: A, @@ -22,7 +29,7 @@ where impl Decoder where I: Iterator, - A: UnveilAlgorithm, + A: UnveilAlgorithm, { pub fn new(input: I, algorithm: A) -> Self { Decoder { input, algorithm } @@ -32,7 +39,7 @@ where impl Read for Decoder where I: Iterator, - A: UnveilAlgorithm, + A: UnveilAlgorithm, { fn read(&mut self, buf: &mut [u8]) -> Result { // TODO better let the algorithm determine the density of decoding @@ -58,9 +65,10 @@ where } /// default 1 bit unveil strategy +#[derive(Debug)] pub struct OneBitUnveil; -impl UnveilAlgorithm for OneBitUnveil { - #[inline(always)] +impl UnveilAlgorithm for OneBitUnveil { + #[inline] fn decode(&self, carrier: MediaPrimitive) -> bool { match carrier { MediaPrimitive::ImageColorChannel(b) => (b & 0x1) > 0, diff --git a/stegano-core/src/universal_encoder.rs b/stegano-core/src/universal_encoder.rs index f037359..dec142d 100644 --- a/stegano-core/src/universal_encoder.rs +++ b/stegano-core/src/universal_encoder.rs @@ -1,7 +1,8 @@ use bitstream_io::{BitRead, BitReader, LittleEndian}; +use enum_dispatch::enum_dispatch; use std::io::{Cursor, Result, Write}; -use crate::{HideBit, MediaPrimitive, MediaPrimitiveMut}; +use crate::{MediaPrimitive, MediaPrimitiveMut}; /// abstracting write back of a carrier item pub trait WriteCarrierItem { @@ -9,40 +10,52 @@ pub trait WriteCarrierItem { fn flush(&mut self) -> Result<()>; } +#[enum_dispatch] +pub enum HideAlgorithms { + OneBitHide, + OneBitInLowFrequencyHide, +} + /// generic hiding algorithm, used for specific ones like LSB -pub trait HideAlgorithm { +#[enum_dispatch(HideAlgorithms)] +pub trait HideAlgorithm { /// encodes one bit onto a carrier T e.g. u8 or i16 - fn encode(&self, carrier: T, information: &Result) -> T; + fn encode<'c>(&self, carrier: MediaPrimitiveMut<'c>, information: &Result); } /// generic stegano encoder -pub struct Encoder<'c, C> +pub struct Encoder<'c, C, A> where C: Iterator>, + A: HideAlgorithm, { pub carrier: C, + pub algorithm: A, } -impl<'c, C> Encoder<'c, C> +impl<'c, C, A> Encoder<'c, C, A> where C: Iterator>, + A: HideAlgorithm, { - pub fn new(carrier: C) -> Self { - Encoder { carrier } + pub fn new(carrier: C, algorithm: A) -> Self { + Encoder { carrier, algorithm } } } -impl<'c, C> Write for Encoder<'c, C> +impl<'c, C, A> Write for Encoder<'c, C, A> where C: Iterator>, + A: HideAlgorithm, { + #[inline] fn write(&mut self, buf: &[u8]) -> Result { // TODO better let the algorithm determine the density of encoding let items_to_take = buf.len() << 3; // 1 bit per sample <=> * 8 <=> << 3 let mut bit_iter = BitReader::endian(Cursor::new(buf), LittleEndian); let mut bit_written: usize = 0; for s in self.carrier.by_ref().take(items_to_take) { - s.hide_bit(bit_iter.read_bit().unwrap()).unwrap(); + self.algorithm.encode(s, &bit_iter.read_bit()); bit_written += 1; } @@ -55,19 +68,103 @@ where } /// default 1 bit hiding strategy +#[derive(Debug)] pub struct OneBitHide; -impl HideAlgorithm for OneBitHide { - fn encode(&self, carrier: MediaPrimitive, information: &Result) -> MediaPrimitive { - match information { - Err(_) => carrier, - Ok(bit) => match carrier { - MediaPrimitive::ImageColorChannel(b) => MediaPrimitive::ImageColorChannel( - (b & (u8::MAX - 1)) | if *bit { 1 } else { 0 }, - ), - MediaPrimitive::AudioSample(b) => { - MediaPrimitive::AudioSample((b & (i16::MAX - 1)) | if *bit { 1 } else { 0 }) +impl HideAlgorithm for OneBitHide { + #[inline(always)] + fn encode<'c>(&self, carrier: MediaPrimitiveMut<'c>, information: &Result) { + if let Ok(bit) = information { + match carrier { + MediaPrimitiveMut::ImageColorChannel(b) => { + *b = ((*b) & (u8::MAX - 1)) | if *bit { 1 } else { 0 } } - }, + MediaPrimitiveMut::AudioSample(b) => { + *b = ((*b) & (i16::MAX - 1)) | if *bit { 1 } else { 0 } + } + _ => {} + } + } + } +} + +/// 1 bit hiding strategy, but +#[derive(Debug)] +pub struct OneBitInLowFrequencyHide; +impl HideAlgorithm for OneBitInLowFrequencyHide { + #[inline(always)] + fn encode<'c>(&self, carrier: MediaPrimitiveMut<'c>, information: &Result) { + if let Ok(bit) = information { + match carrier { + MediaPrimitiveMut::ImageColorChannel(b) => { + *b = ((*b) & 0b11110000) | if *bit { 0b00001111 } else { 0 } + } + MediaPrimitiveMut::AudioSample(b) => { + *b = ((*b) & (0b11111111 << 8)) | if *bit { 0b000000011111111 } else { 0 } + } + _ => {} + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_encode_in_lower_frequencies() { + let encoder = OneBitInLowFrequencyHide; + let mut data = 0b11001101; + { + let mp = MediaPrimitiveMut::ImageColorChannel(&mut data); + encoder.encode(mp, &Ok(true)); + } + assert_eq!(data, 0b11001111); + } + + #[test] + fn should_not_harm_on_error() { + let encoder = OneBitHide; + let mut data = 0b00001110; + { + let mp = MediaPrimitiveMut::ImageColorChannel(&mut data); + encoder.encode( + mp, + &Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)), + ); + } + assert_eq!(data, 0b00001110); + } + + #[test] + fn should_encode_one_bit() { + let encoder = OneBitHide; + let mut data = 0b00001110; + { + let mp = MediaPrimitiveMut::ImageColorChannel(&mut data); + encoder.encode(mp, &Ok(true)); + } + assert_eq!(data, 0b00001111); + + let mut data = 0b00001110; + { + let mp = MediaPrimitiveMut::AudioSample(&mut data); + encoder.encode(mp, &Ok(true)); + } + assert_eq!(data, 0b00001111); + + let mut data = 0b00001110; + { + let mp = MediaPrimitiveMut::ImageColorChannel(&mut data); + encoder.encode(mp, &Ok(false)); + } + assert_eq!(data, 0b00001110); + + let mut data = 0b00001110; + { + let mp = MediaPrimitiveMut::AudioSample(&mut data); + encoder.encode(mp, &Ok(false)); } + assert_eq!(data, 0b00001110); } }