From ba1eb65ffa64dd4d814950dd083d53a88f69513b Mon Sep 17 00:00:00 2001 From: Orlando Valverde Date: Sat, 24 Sep 2022 23:29:30 -0500 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 385 +++++++++++++++++++++++++++++++++++ Cargo.toml | 22 ++ LICENSE-APACHE | 201 ++++++++++++++++++ LICENSE-MIT | 21 ++ README.md | 109 ++++++++++ src/cli.rs | 90 ++++++++ src/image/mod.rs | 24 +++ src/image/open.rs | 42 ++++ src/image/save.rs | 32 +++ src/lib.rs | 12 ++ src/main.rs | 10 + src/steganography/carrier.rs | 88 ++++++++ src/steganography/decode.rs | 124 +++++++++++ src/steganography/encode.rs | 57 ++++++ src/steganography/lsb.rs | 93 +++++++++ src/steganography/mod.rs | 34 ++++ src/steganography/payload.rs | 71 +++++++ tests/files/carrier.png | Bin 0 -> 59230 bytes tests/files/ferris.png | Bin 0 -> 19182 bytes tests/integration.rs | 24 +++ 21 files changed, 1440 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT create mode 100644 README.md create mode 100644 src/cli.rs create mode 100644 src/image/mod.rs create mode 100644 src/image/open.rs create mode 100644 src/image/save.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/steganography/carrier.rs create mode 100644 src/steganography/decode.rs create mode 100644 src/steganography/encode.rs create mode 100644 src/steganography/lsb.rs create mode 100644 src/steganography/mod.rs create mode 100644 src/steganography/payload.rs create mode 100644 tests/files/carrier.png create mode 100644 tests/files/ferris.png create mode 100644 tests/integration.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fbc1f1e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,385 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anyhow" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "image" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "libc" +version = "0.2.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" + +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "png" +version = "0.17.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" +dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "stega" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "image", + "tempfile", + "thiserror", +] + +[[package]] +name = "syn" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" + +[[package]] +name = "thiserror" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2482b07 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "stega" +version = "0.1.0" +authors = ["Orlando Valverde "] +edition = "2021" +license = "MIT OR Apache-2.0" +description = "A simple tool and library to conceal and reveal UTF-8 encoded data within PNG images" +homepage = "https://github.com/septum/stega" +repository = "https://github.com/septum/stega" +documentation = "https://docs.rs/stega" +readme = "README.md" +keywords = ["steganography", "conceal", "reveal", "utf-8", "png"] +categories = ["command-line-utilities", "encoding"] + +[dependencies] +anyhow = "1.0.65" +clap = { version = "3.2.21", default-features = false, features = ["derive", "std"]} +image = { version = "0.24.3", default-features = false , features = ["png"]} +thiserror = "1.0.33" + +[dev-dependencies] +tempfile = "3.3.0" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..9647ed2 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Orlando Valverde + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..9a3bc54 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Orlando Valverde + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7292fc3 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# STEGA + +[![crates.io](https://img.shields.io/crates/v/stega)](https://crates.io/crates/stega) +[![license](https://img.shields.io/crates/l/stega)](https://crates.io/crates/stega) + +A simple tool and library to conceal and reveal UTF-8 encoded data within PNG images. + +## Disclaimer + +This tool and/or library does not guarantee the confidentiality of the data +concealed in the resulting carrier images. Use this crate under your own risk. + +## Library + +Consult the [documentation](https://docs.rs/stega) for more information. + +## Installation + +You must install [Rust](https://www.rust-lang.org/tools/install) on +your system for any of the next installation methods to work: + +### From crates.io + +```shell +$ cargo install stega +``` + +### From GitHub + +```shell +$ cargo install --git https://github.com/septum/stega +``` + +### From source + +```shell +$ git clone https://github.com/septum/stega.git +$ cd stega +$ cargo install --path . +``` + +## Usage + +STEGA has two subcommands available: + +### Conceal + +Using this subcommand will conceal UTF-8 encoded data into a PNG image: + +```shell +$ stega conceal [DATA] +``` + +On success, it will save the data-concealed PNG image in the same location as +the the original image with the filename `carrier.png`, overwriting an already +existing file. + +#### Arguments + +- ``: Valid PNG image path +- `[DATA]`: Optional UTF-8 encoded text argument (with a fallback through STDIN) + +#### Examples + +```shell +$ stega conceal ferris.png "🦀" +``` + +```shell +$ cat hello_world.txt | stega conceal image.png +``` + +### Reveal + +Using this subcommand will reveal UTF-8 encoded data concealed in a PNG image: + +```shell +$ stega reveal +``` + +On success, it will print the data to STDOUT. + +#### Arguments + +- ``: Valid PNG image path + +#### Examples + +```shell +$ stega reveal carrier.png +``` + +```shell +$ stega reveal carrier.png > data.txt +``` + +## License + +This project is dual-licensed under either: + +- MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) +- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) + +at your option. + +## Contributing + +Contributions are very much welcome! If you find a bug, want a new feature or +see an improvement opportunity, please open an issue or submit a PR. diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..f7f569b --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,90 @@ +use std::{ + io::{stdin, BufRead}, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use stega::{decode, encode, open_image, save_image, Carrier, Payload}; + +const DEFAULT_CARRIER_FILENAME: &str = "carrier.png"; + +#[derive(Parser)] +#[clap( + version, + about = "A simple tool to conceal and reveal UTF-8 encoded data within PNG images" +)] +pub struct Cli { + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Subcommand)] +#[clap(arg_required_else_help = true)] +pub enum Command { + /// Conceals UTF-8 encoded data into a PNG image + Conceal { + /// Valid PNG image path + #[clap(value_parser)] + image_path: PathBuf, + /// Optional UTF-8 encoded text argument (with a fallback through STDIN) + #[clap(value_parser)] + data: Option, + }, + /// Reveals UTF-8 encoded data concealed in a PNG image + Reveal { + #[clap(value_parser)] + /// Valid PNG image path + image_path: PathBuf, + }, +} + +impl Cli { + pub fn process_command(&self) -> Result<()> { + if let Some(command) = &self.command { + match command { + Command::Conceal { data, image_path } => { + if let Some(data) = data { + Cli::conceal(&data, image_path)?; + } else { + let data = Cli::stdin_data()?; + Cli::conceal(&data, image_path)?; + } + } + Command::Reveal { image_path } => { + Cli::reveal(image_path)?; + } + } + } + Ok(()) + } + + fn reveal(image_path: &Path) -> Result<()> { + let rgb_image = open_image(image_path)?; + let carrier = Carrier::new(rgb_image)?; + let data = decode(&carrier)?; + print!("{data}"); + Ok(()) + } + + fn conceal(data: &str, image_path: &Path) -> Result<()> { + let rgb_image = open_image(image_path)?; + let payload = Payload::new(data); + let mut carrier = Carrier::new(rgb_image)?; + encode(&payload, &mut carrier)?; + + let rgb_image = carrier.unwrap(); + let carrier_path = image_path.with_file_name(DEFAULT_CARRIER_FILENAME); + save_image(&rgb_image, &carrier_path)?; + Ok(()) + } + + fn stdin_data() -> Result { + let mut stdin = stdin().lock(); + let buffer = stdin.fill_buf()?; + let amt = buffer.len(); + let data = String::from_utf8(buffer.to_vec())?; + stdin.consume(amt); + Ok(data) + } +} diff --git a/src/image/mod.rs b/src/image/mod.rs new file mode 100644 index 0000000..594b422 --- /dev/null +++ b/src/image/mod.rs @@ -0,0 +1,24 @@ +//! Abstraction module to simplify the file operations on a PNG image. + +mod open; +mod save; + +pub use open::open_image; +pub use save::save_image; + +use thiserror::Error; + +/// Errors that can occur while opening or saving an image +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum ImageError { + /// File has an invalid extension + #[error("File has an invalid extension")] + InvalidExtension, + /// An IO error occurred + #[error("IO error: {0}")] + IoError(#[from] std::io::Error), + /// An image error occurred + #[error("Image error: {0}")] + ImageError(#[from] image::ImageError), +} diff --git a/src/image/open.rs b/src/image/open.rs new file mode 100644 index 0000000..1bc94bf --- /dev/null +++ b/src/image/open.rs @@ -0,0 +1,42 @@ +use std::{ffi::OsStr, fs::File, io::BufReader, path::Path}; + +use image::{self, ImageFormat, RgbImage}; + +use super::ImageError; + +/// Opens an PNG image file as an RGB image +pub fn open_image(path: &Path) -> Result { + if matches!(path.extension().and_then(OsStr::to_str), Some("png")) { + let file = File::open(path)?; + let image = image::load(BufReader::new(file), ImageFormat::Png)?; + Ok(image.into_rgb8()) + } else { + Err(ImageError::InvalidExtension) + } +} + +#[cfg(test)] +mod tests { + use image::{self, EncodableLayout, RgbImage}; + use tempfile::Builder; + + use super::*; + + #[test] + fn verify_open_image() { + let tmpfile = Builder::new().suffix(".png").tempfile().unwrap(); + let rgb_image = RgbImage::new(5, 5); + + image::save_buffer( + tmpfile.path(), + rgb_image.as_bytes(), + 5, + 5, + image::ColorType::Rgb8, + ) + .unwrap(); + + let result = open_image(tmpfile.path()); + assert!(result.is_ok()); + } +} diff --git a/src/image/save.rs b/src/image/save.rs new file mode 100644 index 0000000..be5485d --- /dev/null +++ b/src/image/save.rs @@ -0,0 +1,32 @@ +use std::{ffi::OsStr, path::Path}; + +use image::{self, ImageFormat, RgbImage}; + +use super::ImageError; + +/// Saves an RGB image in the provided path +pub fn save_image(rgb_image: &RgbImage, path: &Path) -> Result<(), ImageError> { + if matches!(path.extension().and_then(OsStr::to_str), Some("png")) { + rgb_image.save_with_format(path, ImageFormat::Png)?; + Ok(()) + } else { + Err(ImageError::InvalidExtension) + } +} + +#[cfg(test)] +mod tests { + use image::{self, RgbImage}; + use tempfile::Builder; + + use super::*; + + #[test] + fn verify_save_image() { + let tmpfile = Builder::new().suffix(".png").tempfile().unwrap(); + let rgb_image = RgbImage::new(5, 5); + + let result = save_image(&rgb_image, tmpfile.path()); + assert!(result.is_ok()); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..81247e5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +//! A simple library to conceal and reveal UTF-8 encoded data within PNG images. + +#![deny(missing_docs)] +#![deny(clippy::all)] +#![deny(clippy::cargo)] + +mod image; +mod steganography; + +pub use crate::image::{open_image, save_image, ImageError}; + +pub use crate::steganography::{decode, encode, Carrier, Payload, SteganographyError}; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6002ef0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,10 @@ +mod cli; + +use anyhow::Result; +use clap::Parser; + +use cli::Cli; + +fn main() -> Result<()> { + Cli::parse().process_command() +} diff --git a/src/steganography/carrier.rs b/src/steganography/carrier.rs new file mode 100644 index 0000000..152fb34 --- /dev/null +++ b/src/steganography/carrier.rs @@ -0,0 +1,88 @@ +use std::slice::ChunksExact; + +use image::RgbImage; + +use super::{lsb, SteganographyError}; + +const MIN_CARRIER_CAPACITY: usize = 27; + +/// Image that conceals or will conceal a payload +pub struct Carrier(RgbImage); + +impl Carrier { + /// Create a new carrier by wrapping an RGB image + pub fn new(rgb_image: RgbImage) -> Result { + if rgb_image.len() >= MIN_CARRIER_CAPACITY { + Ok(Self(rgb_image)) + } else { + Err(SteganographyError::SmallCarrier) + } + } + + /// Get the available space in a carrier + pub fn capacity(&self) -> usize { + self.0.len() + } + + /// Get an iterator of each color for every pixel + pub fn subpixels(&mut self) -> impl Iterator + ExactSizeIterator { + self.0.iter_mut() + } + + /// Get and iterator of all the byte-worth chunks from a payload + pub fn payload_chunks(&self) -> ChunksExact<'_, u8> { + self.0.chunks_exact(lsb::BITS_PER_BYTE) + } + + /// Get the contained RGB image + pub fn unwrap(self) -> RgbImage { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const CHUNKS_IN_MIN_CAPACITY_CARRIER: usize = 3; + + #[test] + fn create_new_carrier() { + let rgb_image = RgbImage::new(3, 3); + let result = Carrier::new(rgb_image); + assert!(result.is_ok()); + } + + #[test] + fn try_to_create_new_carrier() { + let rgb_image = RgbImage::new(3, 2); + let result = Carrier::new(rgb_image); + assert!(result.is_err()); + + let error = result.err().unwrap(); + assert!(matches!(error, SteganographyError::SmallCarrier)); + } + + #[test] + fn verify_capacity() { + let rgb_image = RgbImage::new(3, 3); + let result = Carrier::new(rgb_image); + assert!(result.is_ok()); + + let result = result.unwrap(); + assert_eq!(result.capacity(), MIN_CARRIER_CAPACITY); + } + + #[test] + fn verify_chunks_size() { + let rgb_image = RgbImage::new(3, 3); + let result = Carrier::new(rgb_image); + assert!(result.is_ok()); + + let result = result.unwrap(); + assert_eq!( + result.payload_chunks().len(), + CHUNKS_IN_MIN_CAPACITY_CARRIER + ); + } +} diff --git a/src/steganography/decode.rs b/src/steganography/decode.rs new file mode 100644 index 0000000..41df9bc --- /dev/null +++ b/src/steganography/decode.rs @@ -0,0 +1,124 @@ +use super::{carrier::Carrier, lsb, payload::Payload, SteganographyError}; + +/// Reveal UTF-8 encoded data within a carrier by reading its least significant bits +pub fn decode(carrier: &Carrier) -> Result { + let mut payload_bytes = carrier.payload_chunks().map(lsb::decode); + + if payload_bytes.next().filter(Payload::is_stx).is_none() { + return Err(SteganographyError::MalformedPayload); + } + + let text_bytes_length_limit = payload_bytes.len(); + let text_bytes: Vec = payload_bytes + .map_while(|byte| Payload::not_etx(&byte).then_some(byte)) + .collect(); + + if text_bytes.len() < text_bytes_length_limit { + Ok(String::from_utf8(text_bytes)?) + } else { + Err(SteganographyError::MalformedPayload) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use image::RgbImage; + + const HASH_CHAR: char = '#'; + const CRAB_EMOJI: char = '🦀'; + + const HASH_CHAR_CARRIER: [u8; 27] = [ + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, + ]; + const CRAB_EMOJI_CARRIER: [u8; 54] = [ + 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, + 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, + ]; + const HASH_CHAR_TRAILING_CARRIER: [u8; 36] = [ + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ]; + const HASH_CHAR_NO_STX_CARRIER: [u8; 27] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, + ]; + const HASH_CHAR_NO_ETX_CARRIER: [u8; 27] = [ + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + const INVALID_UTF8_CARRIER: [u8; 54] = [ + 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, + ]; + + #[test] + fn decode_char_payload() { + let rgb_image = RgbImage::from_vec(3, 3, HASH_CHAR_CARRIER.into()).unwrap(); + let carrier = Carrier::new(rgb_image).unwrap(); + + let result = decode(&carrier); + assert!(result.is_ok()); + + let payload = result.unwrap(); + assert_eq!(payload, HASH_CHAR.to_string()); + } + + #[test] + fn decode_emoji_payload() { + let rgb_image = RgbImage::from_vec(6, 3, CRAB_EMOJI_CARRIER.into()).unwrap(); + let carrier = Carrier::new(rgb_image).unwrap(); + + let result = decode(&carrier); + assert!(result.is_ok()); + + let payload = result.unwrap(); + assert_eq!(payload, CRAB_EMOJI.to_string()); + } + + #[test] + fn decode_trailing_carrier_payload() { + let rgb_image = RgbImage::from_vec(3, 3, HASH_CHAR_TRAILING_CARRIER.into()).unwrap(); + let carrier = Carrier::new(rgb_image).unwrap(); + + let result = decode(&carrier); + assert!(result.is_ok()); + + let payload = result.unwrap(); + assert_eq!(payload, HASH_CHAR.to_string()); + } + + #[test] + fn try_to_decode_no_stx_carrier() { + let rgb_image = RgbImage::from_vec(3, 3, HASH_CHAR_NO_STX_CARRIER.into()).unwrap(); + let carrier = Carrier::new(rgb_image).unwrap(); + + let result = decode(&carrier); + assert!(result.is_err()); + + let error = result.err().unwrap(); + assert!(matches!(error, SteganographyError::MalformedPayload)); + } + + #[test] + fn try_to_decode_no_etx_carrier() { + let rgb_image = RgbImage::from_vec(3, 3, HASH_CHAR_NO_ETX_CARRIER.into()).unwrap(); + let carrier = Carrier::new(rgb_image).unwrap(); + + let result = decode(&carrier); + assert!(result.is_err()); + + let error = result.err().unwrap(); + assert!(matches!(error, SteganographyError::MalformedPayload)); + } + + #[test] + fn try_to_decode_invalid_utf8_carrier() { + let rgb_image = RgbImage::from_vec(6, 3, INVALID_UTF8_CARRIER.into()).unwrap(); + let carrier = Carrier::new(rgb_image).unwrap(); + + let result = decode(&carrier); + assert!(result.is_err()); + + let error = result.err().unwrap(); + assert!(matches!(error, SteganographyError::Utf8Error(_))); + } +} diff --git a/src/steganography/encode.rs b/src/steganography/encode.rs new file mode 100644 index 0000000..cc08773 --- /dev/null +++ b/src/steganography/encode.rs @@ -0,0 +1,57 @@ +use super::{carrier::Carrier, lsb, payload::Payload, SteganographyError}; + +/// Encodes a payload into a carrier by overwriting in place its least significant bits +pub fn encode(payload: &Payload, carrier: &mut Carrier) -> Result<(), SteganographyError> { + if carrier.capacity() >= payload.length() { + carrier + .subpixels() + .zip(payload.bits()) + .for_each(|(subpixel, bit)| lsb::encode(subpixel, bit)); + Ok(()) + } else { + Err(SteganographyError::SmallCarrier) + } +} + +#[cfg(test)] +mod tests { + use image::RgbImage; + + use super::*; + + const HASH_CHAR: char = '#'; + const SHEBANG_STR: &str = "#!"; + + const HASH_CHAR_CARRIER: [u8; 27] = [ + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, + ]; + + #[test] + fn encode_char_payload() { + let rgb_image = RgbImage::from_vec(3, 3, HASH_CHAR_CARRIER.into()).unwrap(); + let mut carrier = Carrier::new(rgb_image).unwrap(); + let payload = Payload::new(&HASH_CHAR.to_string()); + + let result = encode(&payload, &mut carrier); + assert!(result.is_ok()); + + let result = carrier + .subpixels() + .zip(&HASH_CHAR_CARRIER) + .all(|(a, b)| a == b); + assert!(result); + } + + #[test] + fn try_to_encode_into_small_carrier() { + let rgb_image = RgbImage::new(3, 3); + let mut carrier = Carrier::new(rgb_image).unwrap(); + let payload = Payload::new(&SHEBANG_STR.to_string()); + + let result = encode(&payload, &mut carrier); + assert!(result.is_err()); + + let error = result.err().unwrap(); + assert!(matches!(error, SteganographyError::SmallCarrier)); + } +} diff --git a/src/steganography/lsb.rs b/src/steganography/lsb.rs new file mode 100644 index 0000000..ac99a16 --- /dev/null +++ b/src/steganography/lsb.rs @@ -0,0 +1,93 @@ +const EMPTY_BYTE: u8 = 0b_0000_0000; +const LSB_MASK: u8 = 0b_0000_0001; +const LBS_INDEX: u8 = 0; +const LBS_ZERO_BIT_INDEXING: [u8; BITS_PER_BYTE] = [7, 6, 5, 4, 3, 2, 1, LBS_INDEX]; + +pub const BITS_PER_BYTE: usize = 8; + +pub fn encode(byte: &mut u8, bit: bool) { + if bit { + set_lsb(byte) + } else { + clear_lsb(byte) + } +} + +pub fn byte_to_bits(byte: &u8) -> [bool; BITS_PER_BYTE] { + LBS_ZERO_BIT_INDEXING.map(|index| is_bit_index_set(byte, index)) +} + +pub fn decode(bytes: &[u8]) -> u8 { + bytes + .iter() + .zip(LBS_ZERO_BIT_INDEXING) + .fold(EMPTY_BYTE, |byte, (subpixel, index)| { + if is_bit_index_set(subpixel, LBS_INDEX) { + byte + index_value(index) + } else { + byte + } + }) +} + +fn set_lsb(byte: &mut u8) { + *byte |= LSB_MASK; +} + +fn clear_lsb(byte: &mut u8) { + *byte &= !LSB_MASK; +} + +fn index_value(index: u8) -> u8 { + LSB_MASK << index +} + +fn is_bit_index_set(byte: &u8, index: u8) -> bool { + *byte & index_value(index) != EMPTY_BYTE +} + +#[cfg(test)] +mod tests { + use super::*; + + const ZERO_BYTE: u8 = 0b_0000_0000; + const ONE_BYTE: u8 = 0b_0000_0001; + const TWO_BYTE: u8 = 0b_0000_0010; + const THREE_BYTE: u8 = 0b_0000_0011; + + const EMPTY_BYTE_BITS: [bool; BITS_PER_BYTE] = + [false, false, false, false, false, false, false, false]; + + const SET_BIT: bool = true; + const UNSET_BIT: bool = false; + + const HASH_CHAR_BYTE: u8 = 0b_0010_0011; + const HASH_CHAR_CARRIER_BYTES: [u8; BITS_PER_BYTE] = [ + ZERO_BYTE, ZERO_BYTE, ONE_BYTE, ZERO_BYTE, ZERO_BYTE, ZERO_BYTE, ONE_BYTE, ONE_BYTE, + ]; + + #[test] + fn encode_bytes() { + let mut byte = TWO_BYTE; + + encode(&mut byte, SET_BIT); + assert_eq!(byte, THREE_BYTE); + + encode(&mut byte, UNSET_BIT); + assert_eq!(byte, TWO_BYTE); + } + + #[test] + fn verify_bytes_to_bits() { + let result = byte_to_bits(&EMPTY_BYTE) + .into_iter() + .zip(EMPTY_BYTE_BITS) + .all(|(a, b)| a == b); + assert!(result); + } + + #[test] + fn verify_decode() { + assert_eq!(decode(&HASH_CHAR_CARRIER_BYTES), HASH_CHAR_BYTE); + } +} diff --git a/src/steganography/mod.rs b/src/steganography/mod.rs new file mode 100644 index 0000000..3a2a6f4 --- /dev/null +++ b/src/steganography/mod.rs @@ -0,0 +1,34 @@ +//! Core module that provides the main functionality. + +mod carrier; +mod decode; +mod encode; +mod lsb; +mod payload; + +use std::string::FromUtf8Error; + +pub use carrier::Carrier; + +pub use payload::Payload; + +pub use encode::encode; + +pub use decode::decode; + +use thiserror::Error; + +/// Errors that can occur while encoding or decoding a carrier +#[non_exhaustive] +#[derive(Debug, Error)] +pub enum SteganographyError { + /// Carrier capacity is less than necessary + #[error("Carrier capacity is less than necessary")] + SmallCarrier, + /// Carrier has a malformed payload or no payload at all + #[error("Carrier has a malformed payload or no payload at all")] + MalformedPayload, + /// An UTF-8 conversion error ocurred + #[error("UTF-8 conversion error: {0}")] + Utf8Error(#[from] FromUtf8Error), +} diff --git a/src/steganography/payload.rs b/src/steganography/payload.rs new file mode 100644 index 0000000..256d6d3 --- /dev/null +++ b/src/steganography/payload.rs @@ -0,0 +1,71 @@ +use super::lsb; + +const START_OF_TEXT: char = '\u{02}'; +const END_OF_TEXT: char = '\u{03}'; + +/// UTF-8 encoded data to be concealed in a carrier +pub struct Payload(String); + +impl Payload { + /// Create a new carrier from a string slice + pub fn new(data: &str) -> Self { + Payload(format!("{START_OF_TEXT}{data}{END_OF_TEXT}")) + } + + /// Check if a byte is the start of text delimiter + pub fn is_stx(byte: &u8) -> bool { + *byte == START_OF_TEXT as u8 + } + + /// Check if a byte is not the end of text delimiter + pub fn not_etx(byte: &u8) -> bool { + *byte != END_OF_TEXT as u8 + } + + /// Get its total number of bits + pub fn length(&self) -> usize { + self.0.len() * lsb::BITS_PER_BYTE + } + + /// Get an iterator of the payload bits + pub fn bits(&self) -> impl Iterator + '_ { + self.0.as_bytes().iter().flat_map(lsb::byte_to_bits) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const HASH_CHAR: char = '#'; + const HASH_CHAR_PAYLOAD_LENGTH: usize = 24; + const HASH_CHAR_PAYLOAD_BITS: [bool; HASH_CHAR_PAYLOAD_LENGTH] = [ + false, false, false, false, false, false, true, false, false, false, true, false, false, + false, true, true, false, false, false, false, false, false, true, true, + ]; + + #[test] + fn create_new_payload() { + let payload = Payload::new(&HASH_CHAR.to_string()); + + let result = payload.length(); + assert_eq!(result, HASH_CHAR_PAYLOAD_LENGTH); + + let result = payload + .bits() + .zip(HASH_CHAR_PAYLOAD_BITS) + .all(|(a, b)| a == b); + assert!(result); + } + + #[test] + fn verify_delimiters() { + let byte = START_OF_TEXT as u8; + assert!(Payload::is_stx(&byte)); + assert!(Payload::not_etx(&byte)); + + let byte = END_OF_TEXT as u8; + assert!(!Payload::is_stx(&byte)); + assert!(!Payload::not_etx(&byte)); + } +} diff --git a/tests/files/carrier.png b/tests/files/carrier.png new file mode 100644 index 0000000000000000000000000000000000000000..f9b7facec7eb4304e4fd766b55eb0b345e92649d GIT binary patch literal 59230 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 BDkuN| literal 0 HcmV?d00001 diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..da79fa5 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,24 @@ +use std::path::Path; + +use stega::{decode, encode, open_image, save_image, Carrier, Payload}; + +const PAYLOAD_DATA: &str = "🦀"; +const FERRIS_PATH: &str = "tests/files/ferris.png"; +const CARRIER_PATH: &str = "tests/files/carrier.png"; + +#[test] +fn verify_conceal_functionality() { + let payload = Payload::new(PAYLOAD_DATA); + let rgb_image = open_image(Path::new(FERRIS_PATH)).unwrap(); + let mut carrier = Carrier::new(rgb_image).unwrap(); + + assert!(encode(&payload, &mut carrier).is_ok()); + assert!(save_image(&carrier.unwrap(), Path::new(CARRIER_PATH)).is_ok()); +} + +#[test] +fn verify_reveal_functionality() { + let rgb_image = open_image(Path::new(CARRIER_PATH)).unwrap(); + let carrier = Carrier::new(rgb_image).unwrap(); + assert!(decode(&carrier).is_ok()); +}