Steganography made simple

2008-12-30, , , Comments

What's hidden in this image?

As programmers, our code should be readable, not cryptic; but sometimes it’s fun to surprise, obfuscate or conceal. Wikipedia says:

Steganography is the art and science of writing hidden messages in such a way that no-one apart from the sender and intended recipient even realizes there is a hidden message. By contrast, cryptography obscures the meaning of a message, but it does not conceal the fact that there is a message.

Lemon juice on paper never worked for me, and (as I discovered when I tried to devise a title for an earlier post) it’s hard work hiding a text message as a pattern within a larger text message. Sadly my hair is too fine for Histiaeus’s inspired shave-a-slave trick.

In a digital age, steganographers have it easier. The larger the carrier message the easier it is to disguise a payload within it. My mobile phone has a 3 megapixel camera; I could embed the entire text content of this website (tarred and bzipped) within a single one of its photos without anyone noticing. The Wikipedia page on steganography has a remarkable example of a picture of a tree which, after some bit-shifting, turns into a passable picture of a cat!

The Python Imaging Library (PIL) makes tinkering with images a snip. Here’s a short program to hide messages in images, and to reveal such messages. PIL isn’t ready for Python 3.0 yet, so I’m using 2.6. Note in passing the use of a couple of recent additions to my favourite module, itertools.product and itertools.izip_longest.

PIL steganography
'''Digital image steganography based on the Python Imaging Library (PIL)

Any message can be hidden, provided the image is large enough. The message is
packed into the least significant bits of the pixel colour bands. A 4 byte
header (packed in the same way) carries the message payload length.
'''
import Image
import itertools as its

def n_at_a_time(items, n, fillvalue):
    '''Returns an iterator which groups n items at a time.
    
    Any final partial tuple will be padded with the fillvalue
    
    >>> list(n_at_a_time([1, 2, 3, 4, 5], 2, 'X'))
    [(1, 2), (3, 4), (5, 'X')]
    '''
    it = iter(items)
    return its.izip_longest(*[it] * n, fillvalue=fillvalue)

def biterator(data):
    '''Returns a biterator over the input data.
    
    >>> list(biterator(chr(0b10110101)))
    [1, 0, 1, 1, 0, 1, 0, 1]
    '''
    return ((ord(ch) >> shift) & 1
            for ch, shift in its.product(data, range(7, -1, -1)))

def header(n):
    '''Return n packed in a 4 byte string.'''
    bytes = (chr(n >> s & 0xff) for s in range(24, -8, -8))
    return ('%s' * 4) % tuple(bytes)

def setlsb(cpt, bit):
    '''Set least significant bit of a colour component.'''
    return cpt & ~1 | bit

def hide_bits(pixel, bits):
    '''Hide a bit in each pixel component, returning the resulting pixel.'''
    return tuple(its.starmap(setlsb, zip(pixel, bits)))

def hide_bit(pixel, bit):
    '''Similar to the above, but for single band images.'''
    return setlsb(pixel, bit[0])

def unpack_lsbits_from_image(image):
    '''Unpack least significant bits from image pixels.'''
    # Return depends on number of colour bands. See also hide_bit(s)
    if len(image.getbands()) == 1:
        return (px & 1 for px in image.getdata())
    else:
        return (cc & 1 for px in image.getdata() for cc in px)

def call(f): # (Used to defer evaluation of f)
    return f()

def disguise(image, data):
    '''Disguise data by packing it into an image.
    
    On success, the image is modified and returned to the caller.
    On failure, None is returned and the image is unchanged.
    '''
    payload = '%s%s' % (header(len(data)), data)
    npixels = image.size[0] * image.size[1]
    nbands = len(image.getbands())
    if len(payload) * 8 <= npixels * nbands:
        new_pixel = hide_bit if nbands == 1 else hide_bits
        pixels = image.getdata()
        bits = n_at_a_time(biterator(payload), nbands, 0)
        new_pixels = its.starmap(new_pixel, its.izip(pixels, bits))
        image.putdata(list(new_pixels))
        return image

def reveal(image):
    '''Returns any message disguised in the supplied image file, or None.'''
    bits = unpack_lsbits_from_image(image)
    def accum_bits(n):
        return reduce(lambda a, b: a << 1 | b, its.islice(bits, n), 0)
    def next_ch():
        return chr(accum_bits(8))
    npixels = image.size[0] * image.size[1] 
    nbands = len(image.getbands())
    data_length = accum_bits(32)
    if npixels * nbands > 32 + data_length * 8:
        return ''.join(its.imap(call, its.repeat(next_ch, data_length)))

if __name__ == "__main__":
    import urllib
    droste = urllib.urlopen("http://is.gd/cHqT").read()
    open("droste.png", "wb").write(droste)
    droste = Image.open("droste.png")
    while droste:
        droste.show()
        droste = reveal(droste)
        if droste:
            open("droste.png", "wb").write(droste)
            droste = Image.open("droste.png")

The code is available via anonymous SVN access at http://svn.wordaligned.org/svn/etc/steganography.

☡ For brevity, I haven’t provided a nice user interface to disguise() and reveal(). The short main program is (intentionally) lightly obfuscated. Disguise() modifies the supplied image argument — use Image.copy() if this is a problem. You must also choose a lossless format to save the disguised image: I recommend PNG, but please do check reveal() works on any saved image.


Keen on quines and cocoa?