diff --git a/stegano/console/wav.py b/stegano/console/wav.py new file mode 100644 index 0000000..40cd29d --- /dev/null +++ b/stegano/console/wav.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# Stegano - Stegano is a pure Python steganography module. +# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org +# +# For more information : https://github.com/cedricbonhomme/Stegano +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see + +__author__ = "Cedric Bonhomme" +__version__ = "$Revision: 0.7 $" +__date__ = "$Date: 2016/03/18 $" +__revision__ = "$Date: 2019/06/04 $" +__license__ = "GPLv3" + +try: + from stegano import wav +except Exception: + print("Install stegano: pipx install Stegano") + +import argparse + +from stegano import tools + +def main(): + parser = argparse.ArgumentParser(prog="stegano-lsb") + subparsers = parser.add_subparsers( + help="sub-command help", dest="command", required=True + ) + + # Subparser: Hide + parser_hide = subparsers.add_parser("hide", help="hide help") + # Original audio + parser_hide.add_argument( + "-i", + "--input", + dest="input_audio_file", + required=True, + help="Input audio file.", + ) + parser_hide.add_argument( + "-e", + "--encoding", + dest="encoding", + choices=tools.ENCODINGS.keys(), + default="UTF-8", + help="Specify the encoding of the message to hide." + " UTF-8 (default) or UTF-32LE.", + ) + + group_secret = parser_hide.add_mutually_exclusive_group(required=True) + # Non binary secret message to hide + group_secret.add_argument( + "-m", dest="secret_message", help="Your secret message to hide (non binary)." + ) + # Binary secret message to hide + group_secret.add_argument( + "-f", dest="secret_file", help="Your secret to hide (Text or any binary file)." + ) + + # Audio containing the secret + parser_hide.add_argument( + "-o", + "--output", + dest="output_audio_file", + required=True, + help="Output audio containing the secret.", + ) + + # Subparser: Reveal + parser_reveal = subparsers.add_parser("reveal", help="reveal help") + parser_reveal.add_argument( + "-i", + "--input", + dest="input_audio_file", + required=True, + help="Input audio file.", + ) + parser_reveal.add_argument( + "-e", + "--encoding", + dest="encoding", + choices=tools.ENCODINGS.keys(), + default="UTF-8", + help="Specify the encoding of the message to reveal." + " UTF-8 (default) or UTF-32LE.", + ) + + arguments = parser.parse_args() + + if arguments.command == "hide": + if arguments.secret_message is not None: + secret = arguments.secret_message + elif arguments.secret_file != "": + secret = tools.binary2base64(arguments.secret_file) + + wav.hide( + input_file=arguments.input_audio_file, + message=secret, + encoding=arguments.encoding, + output_file=arguments.output_audio_file + ) + + elif arguments.command == "reveal": + try: + secret = wav.reveal( + encoded_wav=arguments.input_audio_file, + encoding=arguments.encoding + ) + except IndexError: + print("Impossible to detect message.") + exit(0) + if arguments.secret_binary is not None: + data = tools.base642binary(secret) + with open(arguments.secret_binary, "wb") as f: + f.write(data) + else: + print(secret) diff --git a/stegano/exifHeader/exifHeader.py b/stegano/exifHeader/exifHeader.py index 33fec99..1fefa93 100644 --- a/stegano/exifHeader/exifHeader.py +++ b/stegano/exifHeader/exifHeader.py @@ -40,7 +40,7 @@ def hide( from zlib import compress if secret_file is not None: - with open(secret_file) as f: + with open(secret_file, "rb") as f: secret_message = f.read() try: diff --git a/stegano/wav/__init__.py b/stegano/wav/__init__.py new file mode 100644 index 0000000..e2529fe --- /dev/null +++ b/stegano/wav/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +from .wav import hide, reveal + +__all__ = ["hide", "reveal"] diff --git a/stegano/wav/wav.py b/stegano/wav/wav.py new file mode 100644 index 0000000..0593257 --- /dev/null +++ b/stegano/wav/wav.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# Stegano - Stéganô is a basic Python Steganography module. +# Copyright (C) 2010-2024 Cédric Bonhomme - https://www.cedricbonhomme.org +# +# For more information : https://github.com/cedricbonhomme/Stegano +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see + +__author__ = "Cedric Bonhomme" +__version__ = "$Revision: 0.2 $" +__date__ = "$Date: 2010/10/01 $" +__revision__ = "$Date: 2017/02/06 $" +__license__ = "GPLv3" + +import wave +from typing import IO, Union +from stegano import tools + +def hide( + input_file: Union[str, IO[bytes]], + message: str, + output_file: Union[str, IO[bytes]], + encoding: str = "UTF-8" +): + """ + Hide a message (string) in a .wav audio file. + + Use the lsb of each PCM encoded sample to hide the message string characters as ASCII values. + The first eight bits are used for message_length of the string. + """ + message_length = len(message) + assert message_length != 0, "message message_length is zero" + assert message_length < 255, "message is too long" + + output = wave.open(output_file, "wb") + with wave.open(input_file, "rb") as input: + # get .wav params + nchannels, sampwidth, framerate, nframes, comptype, _ = input.getparams() + assert comptype == "NONE", "only uncompressed files are supported" + + nsamples = nframes * nchannels + + message_bits = f"{message_length:08b}" + "".join(tools.a2bits_list(message, encoding)) + assert len(message_bits) <= nsamples, "message is too long" + + # copy over .wav params to output + output.setnchannels(nchannels) + output.setsampwidth(sampwidth) + output.setframerate(framerate) + + # encode message in frames + frames = bytearray(input.readframes(nsamples)) + for i in range(nsamples): + if i < len(message_bits): + if message_bits[i] == "0": + frames[i] = frames[i] & ~1 + else: + frames[i] = frames[i] | 1 + + # write out + output.writeframes(frames) + + +def reveal(input_file: Union[str, IO[bytes]], encoding: str = "UTF-8"): + """ + Find a message in an image. + + Check the lsb of each PCM encoded sample for hidden message characters (ASCII values). + The first eight bits are used for message_length of the string. + """ + message = "" + encoding_len = tools.ENCODINGS[encoding] + with wave.open(input_file, "rb") as input: + nchannels, _, _, nframes, comptype, _ = input.getparams() + assert comptype == "NONE", "only uncompressed files are supported" + + nsamples = nframes * nchannels + frames = bytearray(input.readframes(nsamples)) + + # Read first 8 bits for message length + length_bits = "" + for i in range(8): + length_bits += str(frames[i] & 1) + message_length = int(length_bits, 2) + + # Read message bits + message_bits = "" + for i in range(8, 8 + message_length * encoding_len): + message_bits += str(frames[i] & 1) + + # Convert bits to string + chars = [chr(int(message_bits[i:i+encoding_len], 2)) for i in range(0, len(message_bits), encoding_len)] + message = "".join(chars) + return message diff --git a/tests/sample-files/free-software-song.wav b/tests/sample-files/free-software-song.wav new file mode 100644 index 0000000..405684a Binary files /dev/null and b/tests/sample-files/free-software-song.wav differ diff --git a/tests/test_wav.py b/tests/test_wav.py new file mode 100644 index 0000000..86276d0 --- /dev/null +++ b/tests/test_wav.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# Stegano - Stegano is a pure Python steganography module. +# Copyright (C) 2010-2025 Cédric Bonhomme - https://www.cedricbonhomme.org +# +# For more information : https://github.com/cedricbonhomme/Stegano +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see + +__author__ = "Cedric Bonhomme" +__version__ = "$Revision: 0.1 $" +__date__ = "$Date: 2016/05/19 $" +__license__ = "GPLv3" + +import os +import unittest + +from stegano import wav + + +class TestWav(unittest.TestCase): + def test_hide_empty_message(self): + """ + Test hiding the empty string. + """ + with self.assertRaises(AssertionError): + wav.hide("./tests/sample-files/free-software-song.wav", "", "./audio.wav") + + def test_hide_and_reveal(self): + messages_to_hide = ["a", "foo", "Hello World!", ":Python:"] + + for message in messages_to_hide: + wav.hide("./tests/sample-files/free-software-song.wav", message, "./audio.wav") + clear_message = wav.reveal("./audio.wav") + + self.assertEqual(message, clear_message) + + def test_with_too_long_message(self): + with open("./tests/sample-files/lorem_ipsum.txt") as f: + message = f.read() + with self.assertRaises(AssertionError): + wav.hide("./tests/sample-files/free-software-song.wav", message, "./audio.wav") + + def tearDown(self): + try: + os.unlink("./audio.wav") + except Exception: + pass + + +if __name__ == "__main__": + unittest.main()