This is my implementation of a prng steganography tool written in Python. You can also find the code on GitHub.
Steganography is the art of hiding messages in (images, videos or even audio)
I've decided to make my own steganography program because I wanted to understand how it works. Instead of just using a tool to get what I want, I wanted to fully grasp how these tools work.
My first programs were quite simple but after a few iterations I've come up with a decent program I think. I make use of Cryptography tools to make it even harder to extract the hidden message from the program by encrypting the message with a password. Lastly I added some randomness so each bit from the message to hide is put in a pseudo-random location of the image. The pseudo-random location is also dependend on the password.
It works by seeding the random module and getting the next unique random integer with that seed. Then it puts the bit of the encrypted text in the lsb [Least Significant Bit]. Decryption works sort of the same and at decryption it knows when to stop looking for bits if it finds the endbyte [0]*7
# Imports
from PIL import Image
import numpy as np
import sys
import os
import getopt
import base64
import random
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
# Set location of directory we are working in to load/save files
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
# Methods by Cryptography module used for encrypting and decrypting the message
def get_key(password):
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(password)
return base64.urlsafe_b64encode(digest.finalize())
def encrypt_text(password, token):
f = Fernet(get_key(password))
return f.encrypt(bytes(token))
def decrypt_text(password, token):
f = Fernet(get_key(password))
return f.decrypt(bytes(token))
# Main encryption method
def encrypt(filename, text, magic):
# check whether the text is a file name
if len(text.split('.')[1:]):
text = read_files(os.path.join(__location__, text))
t = [int(x) for x in ''.join(text_ascii(encrypt_text(magic, text)))] + [0]*7 # endbit
try:
# Change format to png because .jpeg and .bmp extensions do not work
filename = change_image_form(filename)
# Load Image
d_old = load_image(filename)
# Check if image can contain the data
if d_old.size < len(t):
print '[*] Image not big enough'
sys.exit(0)
# get new data and save to image
d_new = encrypt_lsb(d_old, magic, t)
save_image(d_new, 'new_'+filename)
except Exception, e:
print str(e)
# Main decryption method
def decrypt(filename, magic):
try:
# Load image
data = load_image(filename)
# Retrieve text
text = decrypt_lsb(data, magic)
print '[*] Retrieved text: \n%s' % decrypt_text(magic, text)
except Exception, e:
print str(e)
# Random methods used in program
def text_ascii(text):
return map(lambda char: '{:07b}'.format(ord(char)), text)
def ascii_text(byte_char):
return chr(int(byte_char, 2))
def next_random(random_list, data):
next_random_number = random.randint(0, data.size-1)
while next_random_number in random_list:
next_random_number = random.randint(0, data.size-1)
return next_random_number
def generate_seed(magic):
seed = 1
for char in magic:
seed *= ord(char)
print '[*] Your magic number is %d' % seed
return seed
# Encrypt via lsb method
def encrypt_lsb(data, magic, text):
print '[*] Starting Encryption'
# We must alter the seed but for now lets make it simple
random.seed(generate_seed(magic))
random_list = []
for i in range(len(text)):
next_random_number = next_random(random_list, data)
random_list.append(next_random_number)
data.flat[next_random_number] = (data.flat[next_random_number] & ~1) | text[i]
print '[*] Finished Encryption'
return data
# Decrypt via lsb method
def decrypt_lsb(data, magic):
print '[*] Starting Decryption'
random.seed(generate_seed(magic))
random_list = []
output = temp_char = ''
for i in range(data.size):
next_random_number = next_random(random_list, data)
random_list.append(next_random_number)
temp_char += str(data.flat[next_random_number] & 1)
if len(temp_char) == 7:
if int(temp_char) > 0:
output += ascii_text(temp_char)
temp_char = ''
else:
print '[*] Finished Decryption'
return output
# File handling methods
def load_image(filename):
img = Image.open(os.path.join(__location__, filename))
img.load()
data = np.asarray(img, dtype="int32")
return data
def save_image(npdata, outfilename):
img = Image.fromarray(np.asarray(np.clip(npdata, 0, 255), dtype="uint8"), "RGB")
img.save(os.path.join(__location__, outfilename))
def change_image_form(filename):
f = filename.split('.')
if not (f[-1] == 'bmp' or f[-1] == 'BMP' or f[-1] == 'PNG' or f[-1] == 'png'):
img = Image.open(os.path.join(__location__, filename))
img.load()
filename = ''.join(f[:-1]) + '.png'
img.save(os.path.join(__location__, filename))
return filename
def read_files(filename):
if os.path.exists(filename):
with open(filename, 'r') as f:
return ''.join([i for i in f])
return filename.replace(__location__, '')[1:]
# Usage and main
def usage():
print "Steganography prng-Tool @Ludisposed & @Qin"
print ""
print "Usage: prng_stego.py -e -m magic filename text "
print "-e --encrypt - encrypt filename with text"
print "-d --decrypt - decrypt filename"
print "-m --magic - encrypt/decrypt with password"
print ""
print ""
print "Examples: "
print "python prng_stego.py -e -m pass test.png howareyou"
print 'python prng_stego.py -e -m magic test.png tester.sh'
print 'python prng_stego.py -e -m magic test.png file_test.txt'
print 'python prng_stego.py --encrypt --magic password test.png "howareyou some other text"'
print ''
print "python prng_stego.py -d -m password test.png"
print "python prng_stego.py --decrypt --magic password test.png"
sys.exit(0)
if __name__ == "__main__":
if not len(sys.argv[1:]):
usage()
try:
opts, args = getopt.getopt(sys.argv[1:], "hedm:", ["help", "encrypt", "decrypt", "magic="])
except getopt.GetoptError as err:
print str(err)
usage()
magic = to_encrypt = None
for o, a in opts:
if o in ("-h", "--help"):
usage()
elif o in ("-e", "--encrypt"):
to_encrypt = True
elif o in ("-d", "--decrypt"):
to_encrypt = False
elif o in ("-m", "--magic"):
magic = a
else:
assert False, "Unhandled Option"
if magic is None or to_encrypt is None:
usage()
if not to_encrypt:
filename = args[0]
decrypt(filename, magic)
else:
filename = args[0]
text = args[1]
encrypt(filename, text, magic)
Things I'd like to improve
- Any general Coding tips are welcome!
- I would also love some tips on how the use the
magic
to create a wierd seed. - Furthermore I'm interested in how Random this is, to my understanding the random module uses Mersenne Twister implementation and that is not cryptographically secure.
- Lastly I think this is harder to decrypt then just normal lsb_stego, but can not prove anything.
EDIT I've made a follow up-questions which can be found here