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/wav/wav.py b/stegano/wav/wav.py index 029257f..0593257 100644 --- a/stegano/wav/wav.py +++ b/stegano/wav/wav.py @@ -36,7 +36,7 @@ def hide( """ Hide a message (string) in a .wav audio file. - Use the lsb of each sample to hide the message string characters as ASCII values. + 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) @@ -45,6 +45,7 @@ def hide( 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" @@ -53,10 +54,12 @@ def hide( 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): @@ -65,6 +68,7 @@ def hide( else: frames[i] = frames[i] | 1 + # write out output.writeframes(frames) @@ -72,7 +76,7 @@ def reveal(input_file: Union[str, IO[bytes]], encoding: str = "UTF-8"): """ Find a message in an image. - Check the lsb of each sample for hidden message characters (ASCII values). + 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 = ""