Background
This is a steganography project I am working on. The code is supposed to do the following:
- Read a mp3 file (in bytes)
- Find the frame headers (via the frame sync bits 0xFF)
- Get and convert a secret message (string) into bits
- Insert the bits into different parts of the frame header, such as bit 24, 29, 30. (with reference to this)
- Write the data back into a new file
- Try and retrieve the data from this new file
Errors encountered
Currently, after writing back to the file, when trying to retrieve the data from the newly written file, a few parts of the text will become garbled. Upon further analysis, it seems that there were some bit flips that occurred. In the following code, only options for 2 bits have been increasing the number of bits just adds a few more function call per bit. This issue occurs once more than one bit is used.
The code
steg.py:
import binascii
import utility
def encode_message(list_of_hex: list, message: str, num_bits: int = 1):
"""Function takes in a list of hexadecimal values in byte format, a message in string format and number of bits
to replace (defaults to 1). Replaces the bytes in place so there is no return"""
message_counter = 0
message = ''.join(f"{ord(i):08b}" for i in message)
length_of_message = len(message)
if length_of_message % num_bits != 0:
message += (num_bits - (length_of_message % num_bits)) * '0'
for i in range(len(list_of_hex)):
if message_counter < length_of_message:
if list_of_hex[i] == b'ff' and list_of_hex[i + 1][:1] == b'f':
if num_bits == 1:
current_byte = utility.convert_byte_hex_to_bin(list_of_hex[i + 2]) # Gets the next 8 bits (17-24)
replaced_byte = utility.encode_bit_0(current_byte, message[message_counter]) # Replace bit 24
message_counter += 1
list_of_hex[i + 2] = utility.convert_bin_to_hex(replaced_byte)
elif num_bits == 2:
current_byte = utility.convert_byte_hex_to_bin(list_of_hex[i + 2]) # Gets the next 8 bits (17-24)
replaced_byte = utility.encode_bit_0(current_byte, message[message_counter]) # Replace bit 24
message_counter += 1
list_of_hex[i + 2] = utility.convert_bin_to_hex(replaced_byte)
# Encodes the next part of the header (Bits 25-32).
current_byte = utility.convert_byte_hex_to_bin(list_of_hex[i + 3]) # Gets the next 8 bits (25-32)
replaced_byte = utility.encode_bit_0(current_byte, message[message_counter]) # Replace bit 32
message_counter += 1
list_of_hex[i + 3] = utility.convert_bin_to_hex(replaced_byte)
elif num_bits == 3:
...
elif num_bits == 4:
...
elif num_bits == 5:
...
elif num_bits == 6: # Expect audio difference from here on out
...
elif num_bits == 7: # Expect audio difference from here on out
...
else:
break
def decode_message(list_of_hex: list, len_message: int, num_bits: int = 1):
"""Function takes in a list of hexadecimal values in byte format, length of the message (in terms of
the number of characters) and number of bits per message (defaults to 1).
Returns a list of binary values (1's and 0's)"""
binary_list = []
current_len = 0
for i in range(len(list_of_hex)):
if list_of_hex[i] == b'ff' and list_of_hex[i + 1][:1] == b'f':
if current_len < len_message * 8:
if num_bits == 1:
current_byte = utility.convert_byte_hex_to_bin(list_of_hex[i + 2]) # Gets the next 8 bits (17-24)
binary_list.append(current_byte[-1]) # inserts bit 24 into list
current_len += 1
elif num_bits == 2:
current_byte = utility.convert_byte_hex_to_bin(list_of_hex[i + 2]) # Gets the next 8 bits (17-24)
binary_list.append(current_byte[-1]) # inserts bit 24 into list
current_len += 1
current_byte = utility.convert_byte_hex_to_bin(list_of_hex[i + 3]) # Gets the next 8 bits (25-32)
binary_list.append(current_byte[-1]) # inserts bit 32 into list
current_len += 1
elif num_bits == 3:
...
elif num_bits == 4:
...
elif num_bits == 5:
...
elif num_bits == 6:
...
elif num_bits == 7:
...
else:
break
return binary_list
def text_from_bits(bits, encoding='utf8', errors='surrogatepass'):
"""Function takes in a string of bits.
Parameters that can be overriden: Encoding and errors
Returns text from a list of binary values (1's and 0's)"""
n = int(bits, 2)
return n.to_bytes((n.bit_length() + 7) // 8, 'big').decode(encoding, errors) or '\0'
def write_secret_to_file(filename: str, secret: str, num_bits: int = 1):
"""Function takes in the filename of mp3 file, secret to be encoded inside and number of bits to use.
Writes the secret message into a new file called secret.mp3 which is created in the same directory"""
encodelist = utility.read_file_into_hex_list(filename)
encode_message(encodelist, secret, num_bits)
with open('secret.mp3', 'wb') as filewrite:
filewrite.write(binascii.unhexlify(b''.join(encodelist)))
def get_secret_from_file(filename: str, msg_len: int, num_bits: int = 1):
"""Function takes in the filename of the mp3 file which has a secret inside, the length of the secret message
(in number of characters) and number of bits to use. Returns the decoded secret"""
decodelist = utility.read_file_into_hex_list(filename)
decoded_byte = decode_message(decodelist, msg_len, num_bits)
decoded_byte_list = [decoded_byte[i:i + 8] for i in range(0, len(decoded_byte), 8)]
char_list = []
for i in decoded_byte_list:
try:
char_list.append(text_from_bits(''.join(i)))
except UnicodeDecodeError:
break
return ''.join(char_list)
utility.py:
import binascii
def encode_bit_0(bin_str: str, bit: str):
"""Function takes in the byte (in terms of binary) to manipulate and a bit to encode (1 or 0). Changes bit 0.
Returns the encoded value as a string """
replaced_byte = bin_str[:-1] + bit # Replaces bit 0 aka LSB
return replaced_byte # padding to ensure 2 digits
def encode_bit_1(bin_str: str, bit: str):
"""Function takes in the byte (in terms of binary) to manipulate and a bit to encode (1 or 0). Changes bit 1.
Returns the encoded value as a string """
replaced_byte = bin_str[:-2] + bit + bin_str[-1] # Encodes bit 1
return replaced_byte
...
def convert_bin_to_hex(bin_str: str):
"""Takes in a binary string (string of 1's and 0's) and formats it into the hexadecimal value"""
return bytes(f'{int(bin_str, 2):x}', 'utf8').zfill(2) # padding to ensure 2 digits
def convert_byte_hex_to_bin(hex_str: bytes):
"""Takes in a hex string in byte form and formats it into a binary string"""
return bin(int(hex_str, 16))[2:].zfill(8)
def read_file_into_hex_list(file: str):
with open(file, 'rb') as fileread:
file_hex_str = binascii.hexlify(fileread.read())
return [file_hex_str[i:i + 2] for i in range(0, len(file_hex_str), 2)]
main.py
import steg
num_bits = 1
msg = 'Test'
steg.write_secret_to_file('audio.mpeg', msg, num_bits)
print(steg.get_secret_from_file('secret.mp3', len(msg), num_bits))