3
\$\begingroup\$

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.

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 or RSA-key. 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 optional and password generated.

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]*8

There are multiple optional encryption/hide techniques like RSA or Fernet(password based) and prng which I did not remove from the program, because.. just password encryption I think have made me vulnerable to brute-force dictionary attacks. Thus I feel it justified to not remove pseudo-randmess although it's not encryption is does make things more difficult to guess the decryption as an attacker, since then you don't know what text to decrypt.

This is a follow up-question: Previous question

Steganography.py Handles all LSB methods

import random
import numpy as np

ascii_text = lambda byte_char: chr(int(byte_char, 2))


def hide_lsb(data, magic, text):
    '''
    A method that hides the least significant bits of the picture with text

    Args:
        data     (list): The list representation of the image
        magic    (str) : The password
        text     (str) : Encrypted text to hide

    Returns:
        The list representation of the image with text hidden in random lsb's
    '''
    print '[*] Hiding message in image'

    if not magic is None:
        insert_fake_data(data)

        # We must alter the seed but for now lets make it simple
        random.seed(generate_seed(magic))

        for char, i in zip(text, random_ints(data.size)):
            data.flat[i] = (data.flat[i] & ~1) | char
    else:
        for i in range(len(text)):
            data.flat[i] = (data.flat[i] & ~1) | text[i]

    print '[*] Finished hiding the message'
    return data

def retrieve_lsb(data, magic):
    '''
    A method that retrieves the least significant bits of the picture

    Args:
        data     (list): The list representation of the image
        magic    (str) : The password

    Returns:
        The list representation of the image with retrieved text from random lsb's
    '''
    print '[*] Retrieving message from image'

    retrieve_range = range(data.size)
    if not magic is None:
        random.seed(generate_seed(magic))
        retrieve_range = random_ints(data.size)

    return retrieve(data, retrieve_range)

def retrieve(data, retrieve_range):
    output = temp_char = ''
    for i in retrieve_range:
            temp_char += str(data.flat[i] & 1)
            if len(temp_char) == 8:
                if int(temp_char) == 0:
                    print '[*] Finished retrieving'
                    return output
                output += ascii_text(temp_char)
                temp_char = ''
    print '[*] Retrieving the message has failed'
    return ''

def generate_seed(magic):
    seed = 1
    for char in magic:
        seed *= ord(char)
    print '[*] Your magic number is %d' % seed
    return seed


def random_ints(size, start=0):
    random_numbers = range(start, size)
    random.shuffle(random_numbers)
    for random_num in random_numbers:
        yield random_num


def insert_fake_data(data):
    print '[*] Inserting fake data'
    for i in random_ints(data.size):
        data.flat[i] = (data.flat[i] & ~1) | random.randint(0,1)
    print '[*] Done inserting fake data'

Encryption.py Handles all encryption methods

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives.asymmetric import padding
import base64

'''
    Ecryption as by Cryptography.RSA module
'''
def gen_key():
    private_key = rsa.generate_private_key(
        public_exponent=65537, key_size=2048, backend=default_backend()
    )
    return private_key


def save_key(pk, filename):
    pem = pk.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.NoEncryption()
    )
    with open(filename, 'wb') as pem_out:
        pem_out.write(pem)


def load_key(filename):
    with open(filename, 'rb') as pem_in:
        pemlines = pem_in.read()
    private_key = load_pem_private_key(pemlines, None, default_backend())
    return private_key


def encrypt_rsa(text, key):
    private_key = load_key(key)
    public_key = private_key.public_key()
    return public_key.encrypt(
        text,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA1()),
            algorithm=hashes.SHA1(),
            label=None
        )
    )


def decrypt_rsa(text, key):
    private_key = load_key(key)
    return private_key.decrypt(
        text,
        padding.OAEP(
            mgf=padding.MGF1(algorithm=hashes.SHA1()),
            algorithm=hashes.SHA1(),
            label=None
        )
    )


'''
   Encryption methods as by Cryptography.fernet module
'''
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))

prng_stego.py Main module

from PIL import Image
import numpy as np
import sys
import os
import getopt
import Steganography
import Encryption


# Gets ascii representation from string to list of bits
text_ascii = lambda text: map(int, ''.join(map(lambda char: '{:08b}'.format(ord(char)), text)))

# Globals
ENDBIT = [0] * 8
PATH = ""


'''
    Filehandling I/O stuff
'''

def file_path_composition(filename):
    if os.path.isfile(filename):
        return os.path.split(filename)
    return ("",filename)


def load_image(filename):
    img = Image.open(os.path.join(PATH, 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(PATH, outfilename))


def change_image_form(filename):
    f = filename.split('.')
    if f[-1] not in ['bmp', 'BMP', 'PNG', 'png']:
        img = Image.open(os.path.join(PATH, filename))
        img.load()
        filename = ''.join(f[:-1]) + '.png'
        img.save(os.path.join(PATH, filename))
    return filename


def check_image_space(text,data):
    if data.size < len(text):
        print '[*] Image not big enough'
        sys.exit(0)


def trans_file_to_text(text):
    path,text = file_path_composition(text)
    filename = os.path.join(path, text)
    if os.path.isfile(filename) and os.path.exists(filename):
        with open(text, 'r') as f:
            text = ''.join([i for i in f])

    return text


# This is bad... but it works
def check_rsa_key(text, filename):
    succes = False
    while not succes:
        e_data = Encryption.encrypt_rsa(text, filename)
        new = ''.join(map(lambda char: '{:08b}'.format(ord(char)), e_data))
        succes = True
        for i in range(0, len(new), 8):
            if new[i:i+8] == '00000000':
                succes = False
    return map(int, new) + ENDBIT


'''
    Main methods and usage
'''
def encrypt(filename, text, password, magic, rsa):
    '''
    A method that hide text into image

    Args:
        filename (str) : The filename of the image
        text     (str) : Text or text file need to be hide in image
        password (str) : Used to encrypt text
        magic    (str) : Used to hide text in image

    Returns:
        A image named new + filename, which with encrypted text in it
    '''
    # Check for file!
    text = trans_file_to_text(text)

    # Optional encrypt
    if not password is None:
        print '[*] Encrypting text'
        text = Encryption.encrypt_text(password, text)

    if not rsa is None:
        print '[*] Encrypting text'
        if rsa == 'new':
            new_key = Encryption.gen_key()
            Encryption.save_key(new_key, 'private_key.pem')

            text = check_rsa_key(text, 'private_key.pem')
            # text = Encryption.encrypt_rsa(text, 'private_key.pem')                    
        else:
            text = check_rsa_key(text, rsa)
            # text = Encryption.encrypt_rsa(text, rsa)

    if rsa is None:        
        text = text_ascii(text) + ENDBIT    

    try:
        # Change format to png
        filename = change_image_form(filename)

        # Load Image
        d_old = load_image(filename)

        # Check if image can contain the data
        check_image_space(text,d_old)

        # get new data and save to image
        d_new = Steganography.hide_lsb(d_old, magic, text)
        save_image(d_new, 'new_'+filename)
    except Exception, e:
        print str(e)


def decrypt(filename, password, magic, rsa):
    '''
    A method that decrypt text from image

    Args:
    filename (str) : The filename of the image
    password (str) : Used to decrypt text
    magic    (str) : Used to retrieve text from image

    Returns:
    Text hided in image
    '''

    try:
        # Load image
        data = load_image(filename)

        # Retrieve text
        text = Steganography.retrieve_lsb(data, magic)

        # Optional Decrypt
        if not password is None:
            print '[*] Decrypting text'
            text = Encryption.decrypt_text(password, text)
        if not rsa is None:
            print '[*] Decrypting text'
            text = Encryption.decrypt_rsa(text, rsa)

        print '[*] Retrieved text: \n%s' % text
    except Exception, e:
        print str(e)


def usage():
    print "Steganography prng-Tool @Ludisposed & @Qin"
    print ""
    print "Usage: prng_stego.py -e -p password -m magic filename text "
    print "-e --encrypt              - encrypt filename with text"
    print "-d --decrypt              - decrypt filename"
    print ''
    print 'Optionals'
    print "-p --password             - encrypt/decrypt with password"
    print "-m --magic                - hide/retrieve   with prng_magic"
    print "-r --rsa                  - encrypt using RSA [filename of key]"
    print ""
    print ""
    print "Examples: "
    print "prng_stego.py -e -p password -m magic test.png howareyou"
    print 'python prng_stego.py -e -p password -m magic test.png tester.sh'
    print 'python prng_stego.py -e -p password -m magic test.png file_test.txt'
    print 'prng_stego.py --encrypt --password password --magic magic test.png "howareyou  some other text"'
    print "prng_stego.py -e test.png howareyou"
    print "prng_stego.py -e -r new test.png howareyou"
    print ''
    print "prng_stego.py -e --rsa private_key.pem new_test.png"
    print "prng_stego.py -d -p password -m magic new_test.png"
    print "prng_stego.py -d new_test.png"
    print "prng_stego.py --decrypt --password password --magic magic new_test.png"
    sys.exit(0)

if __name__ == "__main__":
    if not len(sys.argv[1:]):
        usage()
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hedm:p:r:", ["help", "encrypt", "decrypt", "magic=", "password=", "rsa="])
    except getopt.GetoptError as err:
        print str(err)
        usage()

    magic = to_encrypt = password = rsa = 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
        elif o in ("-p", "--password"):
            password = a
        elif o in ("-r", "--rsa"):
            rsa = a
        else:
            assert False, "Unhandled Option"

    if to_encrypt is None:
        usage()

    filename = args[0]
    PATH, filename = file_path_composition(filename)

    if rsa and password:
        print 'Specify Encryption technique either RSA or Password'
        sys.exit(1)

    if not to_encrypt:
        decrypt(filename, password, magic, rsa)
    else:
        text = args[1]
        encrypt(filename, text, password, magic, rsa)

Things I'd like to improve

  1. I have no Classes whatsoever, so I feel like my approach is not really OOP aproved.
  2. I am having a bug with RSA encryption, and I needed a method check_rsa to be able to fix it. When I encrypt with RSA, my endbyte logic is flawed because some text have [0]*8 in encrypted text. I am currently stuck with how to fix the bug other than the method I made.
  3. I use RSA on lagre blocks of text, while I should be doing AES with RSA encrypted... any links to guide me to a better understanding are helpfull.
  4. General coding tips are always welcome.

Lastly I am working towards more formats to handle like Audio or Video, but first I wanted to complete normal Steganography and make this work perfect. Before I continue, so that is not up for review as it is not yet written.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

As a general comment: I see that you're using PRNG to determine some aspects of your steganography. Rather than random information to determine (for instance) the location of changed bits in the image, you should be extracting that information from the stream being encrypted. In other words, the secret stream that you are hiding, after encryption, should determine the location of the next bit to write, and not a random number.

Another general comment about your steganographic algorithm: you're not restricted to only the least-significant bit of the pixel. There is a technique whereby you can calculate the perceptual cost of a change to a pixel based on the image in its vicinity; in a noisy vicinity you could change many more of the least-significant bits without incurring a noticeable difference.

In general, your usage of Python is not terrible. There are some typos - for example, succes = success; hided = hidden. Also, even if you're using Python 2, I recommend coding for upward compatibility to Python 3, in particular using print(x) instead of print x.

In your usage function you should use a heredoc and a single print statement rather than a handful of print statements.

Correct me if I'm wrong. your save_image method does not enforce a lossy or lossless format. The ability of a steganographic algorithm to retrieve information depends heavily on such format decisions. If you save to JPEG, without some careful tinkering, your encrypted stream will be irrecoverable.

PIL supports many formats that are lossless, not just bmp and png. If you want, you can construct a better list of formats that can be supported. Furthermore, this:

if f[-1] not in ['bmp', 'BMP', 'PNG', 'png']:

Can be this:

if f[-1].lower() not in ('bmp', 'png')
\$\endgroup\$
3
  • \$\begingroup\$ Not using the encrypted text as a tool for prng is on purpose, because I want to split encryption from something not so crypto- like pseudo random. Secondly you have any pointers where to look for how to calculate a pixel's weight? \$\endgroup\$
    – Ludisposed
    Jul 25, 2017 at 17:46
  • \$\begingroup\$ At loading I filter out the .jpeg change_image_form \$\endgroup\$
    – Ludisposed
    Jul 25, 2017 at 17:47
  • 1
    \$\begingroup\$ A good start is ece.uwaterloo.ca/~z70wang/publications/ssim.pdf . \$\endgroup\$
    – Reinderien
    Jul 25, 2017 at 17:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.