Most of the art in building Steganography applications resides in the encoding algorithm. Though before you can conjure up various encoding methods, you need to build a decent framework in which to place the encoding method.
A framework for Steganography applications is fairly straightforward, this source code walkthrough will concern itself with the basic framework of a Steganography application. Python will be the language and development environment for the module. The end result should be a basic Steganography library.
A Steganography library servers two main purposes; either to encode or decode a message.

A Steganography library could be split into separate encoding and decoding modules. Though the encoding and decoding methods are often inverse functions of each other.
PlainInfo is an input, this is normally derived from clearInfo. CoverData is another input, though the minimum amount of CoverData is related to the amount of plainInfo and the process of encoding to be used.
The process is one of encoding the plainInfo with the coverData. The coverData will normally have to be prepared in some way to encode the data.
The output is the resulting stegData.

StegData is the input, the decode method has to relate to the encode method of the StegData.
The process is to decode the StegData.
PlainInfo should then result as the output.

Two main use cases appear with Steganography; encoding and deconding.
This code walkthrough is designed to produce a library that will allow basic Steganography. ClearInfo to PlainInfo and vice versa functions can be added, as well as loading ClearInfo, loading CoverData and saving StegData.
The user case will take into account these extras, and a separate class will be developed to take them into account.
When encoding the user will be able to, give a file name of a text file to be used as ClearInfo, or enter ClearInfo as text directly, give a file name of a coverData image file, and have that loaded as coverData, give a file name by which to save the stegData, ask to encode the plainInfo into the coverData and produce stegData.

When decoding the user will be able to specify a stegData file name to load to decode, give a file name for the resulting plainInfo to be saved in.

The application will model data structures of ClearInfo, ClearInfo filename, PlainInfo, CoverData, CoverData filename, prepared CoverData, StegData, and StegData filename. The CoverData to StegData line will be image based, but the encode and decode method will be kept separate to give some flexibility to adapt to other forms of coverData. The ClearInfo will be text, but yet again the plainInfo will be treated just as standard data.
Both encode and decode could use the same structure, but to allow a higer degree of flexibility in the design better to make each structure a different object for each process, so the data structures are kept in separate classes to facilitate this. The CoverData though will not be used in the decode. We can get back to the prepared CoverData, but there is no way to deduce what the original CoverData would have been. -ed current encoding technique does allow for this though


We have enough analysis done now to crack out some code. So fire up your favourite editor, I will be using vim, but feel free to use what suits you best.
I am going to keep things fairly modular, so I will begin with the standard python setup followed by the basic class framework to be used.
Let's call the module steg.py and store it in a new directory called steg.
Command Line > cd > mkdir steg > cd steg > vi steg.py
The standard python setup.
#!/usr/bin/python
#-----------------------------------------------------------
# _
# __| |_ ___ __ _ _ __ _ _
# (_-< _/ -_) _` |_| '_ \ || |
# /__/\__\___\__, (_) .__/\_, |
# |___/ |_| |__/
#-----------------------------------------------------------
# File : ~/steg/steg.py
# Author : Poised Solutions
# Project : Steganography
# Syntax : python
# Copyright : Poised Solutions Ltd
#-----------------------------------------------------------
# Basic Steganography library
#-----------------------------------------------------------
"""Basic Steganography library designed to act as a source
code walkthrough demonstration of how to build a framework
for a Steganography application."""
#-----------------------------------------------------------
def main():
"""Test steg module."""
pass
#-----------------------------------------------------------
if __name__ == "__main__":
main()
#-----------------------------------------------------------
Test functions added, and major classes identified:
...
#-----------------------------------------------------------
class DataStruct(object):
"""Data structure class."""
def __init__(self):
"""Initialize data structure object."""
pass
#-----------------------------------------------------------
class Encode(DataStruct):
"""Encode class."""
def __init__(self):
"""Initialize encode object."""
pass
#-----------------------------------------------------------
class Decode(DataStruct):
"""Decode class."""
def __init__(self):
"""Initialize decode object."""
pass
#-----------------------------------------------------------
def testEncode():
"""Test encoding."""
print "Testing encode."
#-----------------------------------------------------------
def testDecode():
"""Test decoding."""
print "Testing decode."
#-----------------------------------------------------------
def main():
"""Test steg module."""
print "Testing steg module."
testEncode()
testDecode()
#-----------------------------------------------------------
...
Now some developer magic, a Helper class has been added, the plainInfo should be generic, so a text to plainInfo generator is used. The coverData is going to be an image but again I want to be generic, so the image will be made to iterate through the channel values.
The classes have all been fleshed out, with most of the functionality expressed if not defined completely.
The python imaging library has also been imported.
The basic structure for the module is in place, just the blanks to fill in.
...
import Image
#-----------------------------------------------------------
class DataStruct(object):
"""Data structure class."""
def __init__(self):
"""Initialize data structure object."""
pass
#------------------------------------------------------
def setClearInfo(self, clearInfo):
"""Set clearInfo."""
pass
#------------------------------------------------------
def getClearInfo(self):
"""Get clearInfo."""
pass
#------------------------------------------------------
def setClearInfoFileName(self, clearInfoFN):
"""Set clearInfo file name."""
pass
#------------------------------------------------------
clearInfoFileName = property(
lambda self: self._clearInfoFileName,
setClearInfoFileName,
None,
"ClearInfo file name.")
#------------------------------------------------------
def loadClearInfoTextFile(self):
"""Load ClearInfo from text file."""
pass
#------------------------------------------------------
clearInfo = property(
getClearInfo,
setClearInfo,
None,
"ClearInfo.")
#------------------------------------------------------
def setPlainInfo(self, plainInfo):
"""Set plainInfo."""
pass
#------------------------------------------------------
plainInfo = property(
lambda self: self._plainInfo,
setPlainInfo,
None,
"PlainInfo.")
#------------------------------------------------------
def setCoverData(self, coverData):
"""Set coverData."""
pass
#------------------------------------------------------
coverData = property(
lambda self: self._coverData,
setCoverData,
None,
"CoverData.")
#------------------------------------------------------
def setCoverDataFileName(self, coverDataFN):
"""Set coverData file name."""
pass
#------------------------------------------------------
coverDataFileName = property(
lambda self: self._coverDataFileName,
setCoverDataFileName,
None,
"CoverData file name.")
#------------------------------------------------------
def loadCoverDataImgFile(self):
"""Load coverData from image file."""
pass
#------------------------------------------------------
def setStegDataFileName(self, stegDataFN):
"""Set StegData FileName."""
pass
#------------------------------------------------------
def setStegData(self, stegData):
"""Set StegData."""
pass
#------------------------------------------------------
stegData = property(
lambda self: self._stegData,
setStegData,
None,
"StegData.")
#------------------------------------------------------
stegDataFileName = property(
lambda self: self._stegDataFileName,
setStegDataFileName,
None,
"StegData file name.")
#------------------------------------------------------
def loadStegDataImgFile(self):
"""Load stegData image file."""
pass
#------------------------------------------------------
def saveStegDataImgFile(self):
"""Save stegData image file."""
pass
#-----------------------------------------------------------
class Encode(DataStruct):
"""Encode class."""
def __init__(self):
"""Initialize encode object."""
pass
#------------------------------------------------------
def encode(self):
"""Encode PlainInfo into Prepared CoverData."""
pass
#------------------------------------------------------
def checkReady(self):
"""Check ready to encode."""
# plainInfo given?
# coverData given?
# coverData large enough?
return True
#------------------------------------------------------
def prepareCoverData(self):
"""Prepare CoverData."""
pass
#------------------------------------------------------
def checkCoverData(self):
"""Check CoverData for adequate size."""
pass
#-----------------------------------------------------------
class Decode(DataStruct):
"""Decode class."""
def __init__(self):
"""Initialize decode object."""
pass
#------------------------------------------------------
def decode(self):
"""Decode PlainInfo from StegData."""
pass
#------------------------------------------------------
def checkReady(self):
"""Check ready to decode."""
# stegData given?
return True
#-----------------------------------------------------------
class Helper(object):
"""Steganography helper class."""
@staticmethod
def clearTxt2PlainInfo(txt):
"""Clear Text to PlainInfo."""
pass
#------------------------------------------------------
@staticmethod
def plainInfo2ClearTxt(plainInfo):
"""PlainInfo to Clear Text."""
pass
#------------------------------------------------------
@staticmethod
def img2CoverData(img):
"""Image to CoverData."""
pass
#-----------------------------------------------------------
def testEncode():
"""Test encoding."""
print "Testing Encode class."
encode = Encode()
encode.clearInfo = \
"""Through sheer abandum
fly the messages
through the knowing
into the zip
a deep well
Obscured objects, onomatophobia
an aged amphidiscophoran
down deep dove
Does buck does
I i i
the import subject
to re reproduce"""
# Poised Solutions bears no responsibilty for the
# quality of the above poetry - ed
# philistine - poet
encode.coverDataFileName = \
"coverTest.png"
encode.encode()
encode.stegDataFileName = \
"stegTest.png"
encode.saveStegDataImgFile()
#-----------------------------------------------------------
def testDecode():
"""Test decoding."""
print "Testing Decode Class."
decode = Decode()
decode.stegDataFile = \
"stegTest.png"
decode.decode()
#-----------------------------------------------------------
...
DataStruct class is completed and the testing for it added. ImageDraw module added.
...
import ImageDraw
#-----------------------------------------------------------
class DataStruct(object):
"""Data structure class."""
def __init__(self):
"""Initialize data structure object."""
pass
#------------------------------------------------------
def setClearInfo(self, clearInfo = None):
"""Set clearInfo."""
if clearInfo:
self._clearInfo = clearInfo
print "clearInfo set to '%s'." % (
self.clearInfo)
else:
print "Problem setting clearInfo."
#------------------------------------------------------
def getClearInfo(self):
"""Get clearInfo."""
return self._clearInfo
#------------------------------------------------------
clearInfo = property(
getClearInfo,
setClearInfo,
None,
"ClearInfo.")
#------------------------------------------------------
def setClearInfoFileName(self, clearInfoFN = None):
"""Set clearInfo file name."""
if clearInfoFN:
self._clearInfoFileName = clearInfoFN
print "clearInfoFileName set to '%s'." % (
self.clearInfoFileName)
else:
print "Problem setting clearInfoFileName."
#------------------------------------------------------
clearInfoFileName = property(
lambda self: self._clearInfoFileName,
setClearInfoFileName,
None,
"ClearInfo file name.")
#------------------------------------------------------
def loadClearInfoTextFile(self, clearInfoFN = None):
"""Load ClearInfo from text file."""
if clearInfoFN:
self.clearInfoFileName = clearInfoFN
if not hasattr(self, 'clearInfoFileName'):
print "Problem clearInfoFileName not set."
return
if self.clearInfoFileName == "":
print \
"Problem clearInfoFileName set to " + \
"empty string."
return
try:
fh = open(self.clearInfoFileName, 'r')
txt = fh.read()
except:
print \
"Problem loading clearInfo file '%s'." % (
self.clearInfoFileName)
return
print "clearInfo file loaded '%s'." % (
self.clearInfoFileName)
self.clearInfo = txt
#-----------------------------------------------------
def setPlainInfo(self, plainInfo = None):
"""Set plainInfo."""
if not plainInfo:
print "Problem setting plainInfo."
return
if not isinstance(plainInfo, list):
print "Problem setting plainInfo " + \
"(list type required)."
return
self._plainInfo = plainInfo
print "plainInfo set to '%s'." % (
self.plainInfo)
#------------------------------------------------------
plainInfo = property(
lambda self: self._plainInfo,
setPlainInfo,
None,
"PlainInfo.")
#------------------------------------------------------
def setCoverData(self, coverData = None):
"""Set coverData."""
if not coverData:
print "Problem setting coverData."
return
if not isinstance(coverData, Image.Image):
print "Problem setting coverData " + \
"(Image.Image type required)"
return
self._coverData = coverData
print "coverData set to image being shown"
self.coverData.show()
#------------------------------------------------------
coverData = property(
lambda self: self._coverData,
setCoverData,
None,
"CoverData.")
#------------------------------------------------------
def setCoverDataFileName(self, coverDataFN = None):
"""Set coverData file name."""
if not coverDataFN:
print "Problem setting coverData file name."
return
self._coverDataFileName = coverDataFN
print "coverDataFileName set to '%s'." % (
self.coverDataFileName)
#------------------------------------------------------
coverDataFileName = property(
lambda self: self._coverDataFileName,
setCoverDataFileName,
None,
"CoverData file name.")
#------------------------------------------------------
def loadCoverDataImgFile(self, coverDataFN = None):
"""Load coverData from image file."""
if coverDataFN:
self.coverDataFileName = coverDataFN
if not hasattr(self, 'coverDataFileName'):
print "Problem coverDataFileName not set."
return
if self.coverDataFileName == "":
print \
"Problem coverDataFileName set to empty string."
return
try:
img = Image.open(self.coverDataFileName)
except:
print \
"Problem loading coverData image file " + \
"'%s'." % (
self.coverDataFileName)
return
print "coverData file loaded."
self.coverData = img
#------------------------------------------------------
def setStegData(self, stegData = None):
"""Set StegData."""
if not stegData:
print "Problem setting stegData."
return
if not isinstance(stegData, Image.Image):
print "Problem setting stegData " + \
"(Image.Image type required)"
return
self._stegData = stegData
print "stegData set."
self.stegData.show()
#------------------------------------------------------
stegData = property(
lambda self: self._stegData,
setStegData,
None,
"StegData.")
#------------------------------------------------------
def setStegDataFileName(self, stegDataFN = None):
"""Set StegData FileName."""
if not stegDataFN:
print "Problem setting stegData file name."
return
self._stegDataFileName =stegDataFN
print "stegDataFileName set to '%s'." % (
self.stegDataFileName)
#------------------------------------------------------
stegDataFileName = property(
lambda self: self._stegDataFileName,
setStegDataFileName,
None,
"StegData file name.")
#------------------------------------------------------
def loadStegDataImgFile(self, stegDataFN = None):
"""Load stegData image file."""
if stegDataFN:
self.stegDataFileName = stegDataFN
if not hasattr(self, 'stegDataFileName'):
print "Problem stegDataFileName not set."
return
if self.stegDataFileName == "":
print \
"Problem stegDataFileName set to empty string."
return
try:
img = Image.open(self.stegDataFileName)
except:
print \
"Problem loading stegData image file '%s'." % (
self.stegDataFileName)
return
print "stegData file loaded."
self.stegData = img
#------------------------------------------------------
def saveStegDataImgFile(self, stegDataFN = None):
"""Save stegData image file."""
if stegDataFN:
self.stegDataFileName = stegDataFN
if not hasttr(self, 'stegDataFileName'):
print "Problem stegDataFileName not set."
return
if self.stegDataFileName == "":
print \
"Problem stegDataFileName set to empty string."
return
if not hasattr(self, 'stegData'):
print "Problem no stegData to save."
return
if not isinstance(self.stegData, Image.Image):
print \
"Problem stegData is not of " + \
"type Image.Image"
return
try:
self.stegData.save(self.stegDataFileName)
except:
print \
"Problem saving stegData image file '%s'." % (
self.stegDataFileName)
return
print "stegData file saved."
#-----------------------------------------------------------
...
def testDataStruct():
"""Test DataStruct class."""
print "Testing DataStruct class."
ds = DataStruct()
ds.clearInfo = None
ds.clearInfo = "Hello Steganography."
ds.clearInfoFileName = \
"clearinfo.txt"
ds.loadClearInfoTextFile()
ds.plainInfo = "Fail"
ds.plainInfo = ['a','b','c']
ds.coverData = [1.999, 1.999, 1.999]
ds.coverData = Image.new('RGB', (256, 256), "#007700")
ds.coverData.save('coverdata.png')
ds.coverData = Image.new('RGB', (128, 128), "#007007")
ds.coverDataFileName = \
"coverdata.png"
ds.loadCoverDataImgFile()
ds.stegData = [128, 128, 128]
ds.stegData = Image.new('RGB', (256, 256), "#700700")
draw = ImageDraw.Draw(ds.stegData)
draw.line((0, 0,
ds.stegData.size[0], ds.stegData.size[1]),
fill = 255)
ds.stegDataFileName = \
"stegdata.png"
ds.saveStegDataImgFile()
ds.loadStegDataImgFile()
#-----------------------------------------------------------
def main():
"""Test steg module."""
print "Testing steg module."
testDataStruct()
#testEncode()
#testDecode()
#-----------------------------------------------------------
...
Helper class is completed and testing for it added.
Image generator class added and the idea extended to include StegData images.
PlainInfo bit generator class (note bits are backwards).
...
class Helper(object):
"""Steganography helper class."""
@staticmethod
def clearTxt2PlainInfo(txt):
"""Clear Text to PlainInfo."""
# string to list
plainInfo = list(txt)
return plainInfo
#------------------------------------------------------
@staticmethod
def plainInfo2ClearTxt(plainInfo):
"""PlainInfo to Clear Text."""
#list to string
clearTxt = "".join(plainInfo)
return clearTxt
#------------------------------------------------------
@staticmethod
def plainInfo2bitInfo(plainInfo):
"""PlainInfo to BitInfo."""
bitInfo = BitGen(plainInfo)
return bitInfo
#------------------------------------------------------
@staticmethod
def img2CoverData(img):
"""Image to CoverData."""
coverDataGen = ImgGen(img)
return coverDataGen
#------------------------------------------------------
@staticmethod
def img2StegData(img):
"""Image to StegData."""
stegDataGen = ImgGen(img)
return stegDataGen
#-----------------------------------------------------------
class ImgGen:
"""Image Generator Class."""
def __init__(self, img):
"""Initialize image generating object."""
self.img = img
self.reset()
def next(self):
"""Return next channel value."""
pixelPos = (self.pixel[0], self.pixel[1])
channel = self.channel
try:
value = \
self.img.getpixel(pixelPos
)[channel]
except:
raise "Problem getting next pixel."
self.channel += 1
if self.channel >= 3:
self.channel = 0
self.pixel[0] += 1
if self.pixel[0] >= self.img.size[0]:
self.pixel[0] = 0
self.pixel[1] += 1
if self.pixel[1] >= self.img.size[1]:
return (-1, (-1, -1), -1)
return (value, (pixelPos), channel)
def reset(self):
"""Reset ImgGen."""
self.pixel = [0, 0]
self.channel = 0
self.value = 0
#-----------------------------------------------------------
class BitGen:
"""Bit Generator Class."""
def __init__(self, l):
"""Initialize bit generating object."""
self.l = l
def bit(self):
"""Yield next bit value."""
lpos = 0
bpos = 0
binary = ord(self.l[lpos])
while 1:
yield binary & 1
binary >>= 1
bpos += 1
if bpos == 8:
bpos = 0
lpos += 1
if lpos == len(self.l):
return
binary = ord(self.l[lpos])
#-----------------------------------------------------------
...
def testHelper():
"""Test Helper class."""
print "Testing Helper class."
clearTxt = "Hello there"
print clearTxt
plainInfo = Helper.clearTxt2PlainInfo(clearTxt)
print plainInfo
clearTxt = Helper.plainInfo2ClearTxt(plainInfo)
print clearTxt
img = Image.new('RGB', (16, 16), "#7000a0")
draw = ImageDraw.Draw(img)
draw.line((0, 0,
img.size[0], img.size[1]),
fill = "#ffffff")
draw.line((0, img.size[1],
img.size[0], 0),
fill = "#000000")
coverData = Helper.img2CoverData(img)
print "CoverData Image Channel Values:"
k = 0
cValue = coverData.next()
while cValue[0] != -1:
print "%s" % (cValue[0]),
k += 1
if k == img.size[0] * 3:
k = 0
print
cValue = coverData.next()
bitInfo = Helper.plainInfo2bitInfo(plainInfo)
k = 0
for bit in bitInfo.bit():
print "%s" % (bit),
k += 1
if k == 8:
k = 0
print
#-----------------------------------------------------------
def main():
"""Test steg module."""
print "Testing steg module."
#testDataStruct()
testHelper()
#testEncode()
#testDecode()
#-----------------------------------------------------------
...
Encoding and decoding completed, testing for them added.
class Encode(DataStruct):
"""Encode class."""
def __init__(self):
"""Initialize encode object."""
self.key = "z"
#------------------------------------------------------
def encode(self):
"""Encode PlainInfo into Prepared CoverData."""
if not self.checkReady():
print "Problem encoding."
return
self.prepareBitInfo()
if not self.prepareCoverData():
return False;
self.stegData = self.coverData.copy()
coverData = Helper.img2CoverData(self.coverData)
for bit in self.bitInfo.bit():
value = coverData.next()
color = list(
self.stegData.getpixel(value[1])[:3])
color[value[2]] = value[0] + bit
self.stegData.putpixel(value[1], tuple(color))
#------------------------------------------------------
def checkReady(self):
"""Check ready to encode."""
if hasattr(self, 'clearInfo'):
self.plainInfo = \
Helper.clearTxt2PlainInfo(
self.clearInfo)
if not hasattr(self, 'plainInfo'):
print "Problem no plainInfo for encoding."
return False
if not hasattr(self, 'coverData'):
print "Problem no coverData for encoding."
return False
if not self.checkCoverData():
return False
return True
#------------------------------------------------------
def prepareBitInfo(self):
"""Prepare BitInfo."""
# find the occurance of the first number
firstNum = len(self.plainInfo)
length = str(firstNum)
i = -1
for c in self.plainInfo:
if ord('0') <= ord(c) <= ord('9'):
# 0 - 9 inclusive
firstNum = i
break
i += 1
if firstNum < 0:
firstNum = 0
pos = random.randint(0, firstNum)
length += chr(random.randint(65, 112))
self.plainInfo = \
self.plainInfo[0:pos] + \
list(length) + \
self.plainInfo[pos:]
self.obscurePlainInfo()
self.bitInfo = \
Helper.plainInfo2bitInfo(self.plainInfo)
#------------------------------------------------------
def obscurePlainInfo(self):
"""Obscure PlainInfo."""
# simple cyclic xor obscuring
# hides the numeric length
i = 0
key = self.key
for c in self.plainInfo:
self.plainInfo[i] = chr(ord(c) ^ ord(key))
key = chr(
ord(c) ^ (ord(self.plainInfo[i]) >> 1))
i += 1
print "obscured plainInfo ", self.plainInfo
#------------------------------------------------------
def prepareCoverData(self):
"""Prepare CoverData."""
if not hasattr(self, "coverData"):
print "Problem no coverData supplied."
return False
coverData = Helper.img2CoverData(
self.coverData)
maxLen = len(self.plainInfo) * 8
for k in xrange(0, maxLen):
# baseline is even
value = coverData.next()
color = list(
self.coverData.getpixel(value[1])[:3])
color[value[2]] = value[0] - (value[0] & 1)
self.coverData.putpixel(value[1], tuple(color))
return True
#------------------------------------------------------
def checkCoverData(self):
"""Check CoverData for adequate size."""
# plainInfo size
piSize = len(self.plainInfo) * 8
# length of message length
msgLenLen = (len(str(piSize)) +1) * 8
piSize += msgLenLen
cdSize = \
self.coverData.size[0] * \
self.coverData.size[1] * 3
if piSize > cdSize:
print "Problem coverData not large enough."
return False
return True
#-----------------------------------------------------------
class Decode(DataStruct):
"""Decode class."""
def __init__(self):
"""Initialize decode object."""
self.key = "z"
#------------------------------------------------------
def decode(self):
"""Decode PlainInfo from StegData."""
if not self.checkReady():
print "Problem decoding."
return
stegData = Helper.img2StegData(self.stegData)
c = 0
key = self.key
foundLen = False
plainInfo = []
binStr = ""
i = 0
msgLen = -1
msgLenStr = ""
inLen = False
value = stegData.next()
while value[0] != -1:
# encoded backwards
binStr = str(value[0] & 1) + binStr
c += 1
if c == 8:
c = 0
# character in binStr
#print binStr
ch = chr(int(binStr, 2))
binStr = ""
# descureCharacter
chD = chr(ord(ch) ^ ord(key))
key = chr(ord(chD) ^ (ord(ch) >> 1))
if foundLen:
i += 1
plainInfo += chD
elif ord('0') <= ord(chD) <= ord('9'):
msgLenStr += chD
inLen = True
elif inLen:
foundLen = True
msgLen = int(msgLenStr)
inLen = False
else:
i += 1
plainInfo += chD
if foundLen:
if i >= msgLen:
break
value = stegData.next()
self.plainInfo = plainInfo
self.clearInfo = Helper.plainInfo2ClearTxt(
self.plainInfo)
#------------------------------------------------------
def checkReady(self):
"""Check ready to decode."""
if not hasattr(self, 'stegData'):
print "Problem decoding no stegData"
return False
return True
#-----------------------------------------------------------
Full sourcecode listing, zlib compression added, plainInfo list cut, default testing moved to a new function.
#!/usr/bin/python
#-----------------------------------------------------------
# _
# __| |_ ___ __ _ _ __ _ _
# (_-< _/ -_) _` |_| '_ \ || |
# /__/\__\___\__, (_) .__/\_, |
# |___/ |_| |__/
#-----------------------------------------------------------
# File : ~/steg/steg.py
# Author : Poised Solutions
# Project : Steganography
# Syntax : python
# Copyright : Poised Solutions Ltd
#-----------------------------------------------------------
# Basic Stenganography Library
#-----------------------------------------------------------
"""Basic Steganography library designed to act as a source
code walkthrough demonstration of how to build a framework
for a Steganography application."""
#-----------------------------------------------------------
import Image
import ImageDraw
import random
import zlib
#-----------------------------------------------------------
class DataStruct(object):
"""Data structure class."""
def __init__(self):
"""Initialize data structure object."""
pass
#------------------------------------------------------
def setClearInfo(self, clearInfo = None):
"""Set clearInfo."""
if clearInfo:
self._clearInfo = clearInfo
print "clearInfo set to '%s'." % (
self.clearInfo)
else:
print "Problem setting clearInfo."
#------------------------------------------------------
def getClearInfo(self):
"""Get clearInfo."""
return self._clearInfo
#------------------------------------------------------
clearInfo = property(
getClearInfo,
setClearInfo,
None,
"ClearInfo.")
#------------------------------------------------------
def setClearInfoFileName(self, clearInfoFN = None):
"""Set clearInfo file name."""
if clearInfoFN:
self._clearInfoFileName = clearInfoFN
print "clearInfoFileName set to '%s'." % (
self.clearInfoFileName)
else:
print "Problem setting clearInfoFileName."
#------------------------------------------------------
clearInfoFileName = property(
lambda self: self._clearInfoFileName,
setClearInfoFileName,
None,
"ClearInfo file name.")
#------------------------------------------------------
def loadClearInfoTextFile(self, clearInfoFN = None):
"""Load ClearInfo from text file."""
if clearInfoFN:
self.clearInfoFileName = clearInfoFN
if not hasattr(self, 'clearInfoFileName'):
print "Problem clearInfoFileName not set."
return
if self.clearInfoFileName == "":
print \
"Problem clearInfoFileName set to " + \
"empty string."
return
try:
fh = open(self.clearInfoFileName, 'r')
txt = fh.read()
except:
print \
"Problem loading clearInfo file '%s'." % (
self.clearInfoFileName)
return
print "clearInfo file loaded '%s'." % (
self.clearInfoFileName)
self.clearInfo = txt
#-----------------------------------------------------
def setPlainInfo(self, plainInfo = None):
"""Set plainInfo."""
if not plainInfo:
print "Problem setting plainInfo."
return
if not isinstance(plainInfo, list):
print "Problem setting plainInfo " + \
"(list type required)."
return
self._plainInfo = plainInfo
print "plainInfo set to '%s'." % (
self.plainInfo)
#------------------------------------------------------
plainInfo = property(
lambda self: self._plainInfo,
setPlainInfo,
None,
"PlainInfo.")
#------------------------------------------------------
def setCoverData(self, coverData = None):
"""Set coverData."""
if not coverData:
print "Problem setting coverData."
return
if not isinstance(coverData, Image.Image):
print "Problem setting coverData " + \
"(Image.Image type required)"
return
self._coverData = coverData
print "coverData set to image being shown"
self.coverData.show()
#------------------------------------------------------
coverData = property(
lambda self: self._coverData,
setCoverData,
None,
"CoverData.")
#------------------------------------------------------
def setCoverDataFileName(self, coverDataFN = None):
"""Set coverData file name."""
if not coverDataFN:
print "Problem setting coverData file name."
return
self._coverDataFileName = coverDataFN
print "coverDataFileName set to '%s'." % (
self.coverDataFileName)
#------------------------------------------------------
coverDataFileName = property(
lambda self: self._coverDataFileName,
setCoverDataFileName,
None,
"CoverData file name.")
#------------------------------------------------------
def loadCoverDataImgFile(self, coverDataFN = None):
"""Load coverData from image file."""
if coverDataFN:
self.coverDataFileName = coverDataFN
if not hasattr(self, 'coverDataFileName'):
print "Problem coverDataFileName not set."
return
if self.coverDataFileName == "":
print \
"Problem coverDataFileName set to empty string."
return
try:
img = Image.open(self.coverDataFileName)
except:
print \
"Problem loading coverData image file " + \
"'%s'." % (
self.coverDataFileName)
return
print "coverData file loaded."
self.coverData = img
#------------------------------------------------------
def setStegData(self, stegData = None):
"""Set StegData."""
if not stegData:
print "Problem setting stegData."
return
if not isinstance(stegData, Image.Image):
print "Problem setting stegData " + \
"(Image.Image type required)"
return
self._stegData = stegData
print "stegData set."
self.stegData.show()
#------------------------------------------------------
stegData = property(
lambda self: self._stegData,
setStegData,
None,
"StegData.")
#------------------------------------------------------
def setStegDataFileName(self, stegDataFN = None):
"""Set StegData FileName."""
if not stegDataFN:
print "Problem setting stegData file name."
return
self._stegDataFileName =stegDataFN
print "stegDataFileName set to '%s'." % (
self.stegDataFileName)
#------------------------------------------------------
stegDataFileName = property(
lambda self: self._stegDataFileName,
setStegDataFileName,
None,
"StegData file name.")
#------------------------------------------------------
def loadStegDataImgFile(self, stegDataFN = None):
"""Load stegData image file."""
if stegDataFN:
self.stegDataFileName = stegDataFN
if not hasattr(self, 'stegDataFileName'):
print "Problem stegDataFileName not set."
return
if self.stegDataFileName == "":
print \
"Problem stegDataFileName set to empty string."
return
try:
img = Image.open(self.stegDataFileName)
except:
print \
"Problem loading stegData image file '%s'." % (
self.stegDataFileName)
return
print "stegData file loaded."
self.stegData = img
#------------------------------------------------------
def saveStegDataImgFile(self, stegDataFN = None):
"""Save stegData image file."""
if stegDataFN:
self.stegDataFileName = stegDataFN
if not hasattr(self, 'stegDataFileName'):
print "Problem stegDataFileName not set."
return
if self.stegDataFileName == "":
print \
"Problem stegDataFileName set to empty string."
return
if not hasattr(self, 'stegData'):
print "Problem no stegData to save."
return
if not isinstance(self.stegData, Image.Image):
print \
"Problem stegData is not of " + \
"type Image.Image"
return
try:
self.stegData.save(self.stegDataFileName)
except:
print \
"Problem saving stegData image file '%s'." % (
self.stegDataFileName)
return
print "stegData file saved."
#-----------------------------------------------------------
class Encode(DataStruct):
"""Encode class."""
def __init__(self):
"""Initialize encode object."""
self.key = "z"
#------------------------------------------------------
def encode(self):
"""Encode PlainInfo into Prepared CoverData."""
if not self.checkReady():
print "Problem encoding."
return
self.prepareBitInfo()
if not self.prepareCoverData():
return False;
self.stegData = self.coverData.copy()
coverData = Helper.img2CoverData(self.coverData)
for bit in self.bitInfo.bit():
value = coverData.next()
color = list(
self.stegData.getpixel(value[1])[:3])
color[value[2]] = value[0] + bit
self.stegData.putpixel(value[1], tuple(color))
#------------------------------------------------------
def checkReady(self):
"""Check ready to encode."""
if hasattr(self, 'clearInfo'):
self.plainInfo = \
Helper.clearTxt2PlainInfo(
self.clearInfo)
if not hasattr(self, 'plainInfo'):
print "Problem no plainInfo for encoding."
return False
if not hasattr(self, 'coverData'):
print "Problem no coverData for encoding."
return False
if not self.checkCoverData():
return False
return True
#------------------------------------------------------
def prepareBitInfo(self):
"""Prepare BitInfo."""
# find the occurance of the first number
firstNum = len(self.plainInfo)
length = str(firstNum)
i = -1
for c in self.plainInfo:
if ord('0') <= ord(c) <= ord('9'):
# 0 - 9 inclusive
firstNum = i
break
i += 1
if firstNum < 0:
firstNum = 0
pos = random.randint(0, firstNum)
length += chr(random.randint(65, 112))
self.plainInfo = \
self.plainInfo[0:pos] + \
list(length) + \
self.plainInfo[pos:]
self.obscurePlainInfo()
self.bitInfo = \
Helper.plainInfo2bitInfo(self.plainInfo)
#------------------------------------------------------
def obscurePlainInfo(self):
"""Obscure PlainInfo."""
# simple cyclic xor obscuring
# hides the numeric length
i = 0
key = self.key
for c in self.plainInfo:
self.plainInfo[i] = chr(ord(c) ^ ord(key))
key = chr(
ord(c) ^ (ord(self.plainInfo[i]) >> 1))
i += 1
print "obscured plainInfo ", self.plainInfo
#------------------------------------------------------
def prepareCoverData(self):
"""Prepare CoverData."""
if not hasattr(self, "coverData"):
print "Problem no coverData supplied."
return False
coverData = Helper.img2CoverData(
self.coverData)
maxLen = len(self.plainInfo) * 8
for k in xrange(0, maxLen):
# baseline is even
value = coverData.next()
color = list(
self.coverData.getpixel(value[1])[:3])
color[value[2]] = value[0] - (value[0] & 1)
self.coverData.putpixel(value[1], tuple(color))
return True
#------------------------------------------------------
def checkCoverData(self):
"""Check CoverData for adequate size."""
# plainInfo size
piSize = len(self.plainInfo) * 8
# length of message length
msgLenLen = (len(str(piSize)) +1) * 8
piSize += msgLenLen
cdSize = \
self.coverData.size[0] * \
self.coverData.size[1] * 3
if piSize > cdSize:
print "Problem coverData not large enough."
return False
return True
#-----------------------------------------------------------
class Decode(DataStruct):
"""Decode class."""
def __init__(self):
"""Initialize decode object."""
self.key = "z"
#------------------------------------------------------
def decode(self):
"""Decode PlainInfo from StegData."""
if not self.checkReady():
print "Problem decoding."
return
stegData = Helper.img2StegData(self.stegData)
c = 0
key = self.key
foundLen = False
plainInfo = []
binStr = ""
i = 0
msgLen = -1
msgLenStr = ""
inLen = False
value = stegData.next()
while value[0] != -1:
# encoded backwards
binStr = str(value[0] & 1) + binStr
c += 1
if c == 8:
c = 0
# character in binStr
#print binStr
ch = chr(int(binStr, 2))
binStr = ""
# descureCharacter
chD = chr(ord(ch) ^ ord(key))
key = chr(ord(chD) ^ (ord(ch) >> 1))
if foundLen:
i += 1
plainInfo += chD
elif ord('0') <= ord(chD) <= ord('9'):
msgLenStr += chD
inLen = True
elif inLen:
foundLen = True
msgLen = int(msgLenStr)
inLen = False
else:
i += 1
plainInfo += chD
if foundLen:
if i >= msgLen:
break
value = stegData.next()
self.plainInfo = plainInfo
self.clearInfo = Helper.plainInfo2ClearTxt(
self.plainInfo)
#------------------------------------------------------
def checkReady(self):
"""Check ready to decode."""
if not hasattr(self, 'stegData'):
print "Problem decoding no stegData"
return False
return True
#-----------------------------------------------------------
class Helper(object):
"""Steganography helper class."""
@staticmethod
def clearTxt2PlainInfo(txt):
"""Clear Text to PlainInfo."""
# zip up helps to avoid repeats
# showing in pattern
txt = zlib.compress(txt)
# string to list
plainInfo = list(txt)
print "Compressed plainInfo " + str(plainInfo)
# chop list into two and reglue
# zip will leave an identifier
# at front
mid = len(plainInfo) // 2
plainInfo = plainInfo[mid:] + plainInfo[:mid]
return plainInfo
#------------------------------------------------------
@staticmethod
def plainInfo2ClearTxt(plainInfo):
"""PlainInfo to Clear Text."""
# chop list into two and reglue
# round up on odd
mid = len(plainInfo) // 2
mid += (len(plainInfo) & 1)
plainInfo = plainInfo[mid:] + plainInfo[:mid]
print "Compressed plainInfo " + str(plainInfo)
# list to string
clearTxt = "".join(plainInfo)
# decompress
clearTxt = zlib.decompress(clearTxt)
return clearTxt
#------------------------------------------------------
@staticmethod
def plainInfo2bitInfo(plainInfo):
"""PlainInfo to BitInfo."""
bitInfo = BitGen(plainInfo)
return bitInfo
#------------------------------------------------------
@staticmethod
def img2CoverData(img):
"""Image to CoverData."""
coverDataGen = ImgGen(img)
return coverDataGen
#------------------------------------------------------
@staticmethod
def img2StegData(img):
"""Image to StegData."""
stegDataGen = ImgGen(img)
return stegDataGen
#-----------------------------------------------------------
class ImgGen:
"""Image Generator Class."""
def __init__(self, img):
"""Initialize image generating object."""
self.img = img
self.reset()
def next(self):
"""Return next channel value."""
pixelPos = (self.pixel[0], self.pixel[1])
channel = self.channel
try:
value = \
self.img.getpixel(pixelPos
)[channel]
except:
raise "Problem getting next pixel."
self.channel += 1
if self.channel >= 3:
self.channel = 0
self.pixel[0] += 1
if self.pixel[0] >= self.img.size[0]:
self.pixel[0] = 0
self.pixel[1] += 1
if self.pixel[1] >= self.img.size[1]:
return (-1, (-1, -1), -1)
return (value, (pixelPos), channel)
def reset(self):
"""Reset ImgGen."""
self.pixel = [0, 0]
self.channel = 0
self.value = 0
#-----------------------------------------------------------
class BitGen:
"""Bit Generator Class."""
def __init__(self, l):
"""Initialize bit generating object."""
self.l = l
def bit(self):
"""Yield next bit value."""
lpos = 0
bpos = 0
binary = ord(self.l[lpos])
while 1:
yield binary & 1
binary >>= 1
bpos += 1
if bpos == 8:
bpos = 0
lpos += 1
if lpos == len(self.l):
return
binary = ord(self.l[lpos])
#-----------------------------------------------------------
def testEncode():
"""Test encoding."""
print "Testing Encode class."
encode = Encode()
encode.clearInfo = \
"""Through sheer abandum
fly the messages
through the knowing
into the zip
a deep well
Obscured objects, onomatophobia
an aged amphidiscophoran
dove dove down
Does buck does
I i i
the import subject
to re reproduce"""
# Poised Solutions bears no responsibilty for the
# quality of the above poetry - ed
# philistine - poet
encode.coverDataFileName = \
"coverTest.png"
encode.loadCoverDataImgFile()
encode.encode()
encode.stegDataFileName = \
"stegTest.png"
encode.saveStegDataImgFile()
#-----------------------------------------------------------
def testDecode():
"""Test decoding."""
print "Testing Decode class."
decode = Decode()
decode.stegDataFileName = \
"stegTest.png"
decode.loadStegDataImgFile()
decode.decode()
#-----------------------------------------------------------
def testDataStruct():
"""Test DataStruct class."""
print "Testing DataStruct class."
ds = DataStruct()
ds.clearInfo = None
ds.clearInfo = "Hello Steganography."
ds.clearInfoFileName = \
"clearinfo.txt"
ds.loadClearInfoTextFile()
ds.plainInfo = "Fail"
ds.plainInfo = ['a','b','c']
ds.coverData = [1.999, 1.999, 1.999]
ds.coverData = Image.new('RGB', (256, 256), "#007700")
ds.coverData.save('coverdata.png')
ds.coverData = Image.new('RGB', (128, 128), "#007007")
ds.coverDataFileName = \
"coverdata.png"
ds.loadCoverDataImgFile()
ds.stegData = [128, 128, 128]
ds.stegData = Image.new('RGB', (256, 256), "#700700")
draw = ImageDraw.Draw(ds.stegData)
draw.line((0, 0,
ds.stegData.size[0], ds.stegData.size[1]),
fill = 255)
ds.stegDataFileName = \
"stegdata.png"
ds.saveStegDataImgFile()
ds.loadStegDataImgFile()
#-----------------------------------------------------------
def testHelper():
"""Test Helper class."""
print "Testing Helper class."
clearTxt = "Hello there"
print clearTxt
plainInfo = Helper.clearTxt2PlainInfo(clearTxt)
print plainInfo
clearTxt = Helper.plainInfo2ClearTxt(plainInfo)
print clearTxt
img = Image.new('RGB', (16, 16), "#7000a0")
draw = ImageDraw.Draw(img)
draw.line((0, 0,
img.size[0], img.size[1]),
fill = "#ffffff")
draw.line((0, img.size[1],
img.size[0], 0),
fill = "#000000")
coverData = Helper.img2CoverData(img)
print "CoverData Image Channel Values:"
k = 0
cValue = coverData.next()
while cValue[0] != -1:
print "%s" % (cValue[0]),
k += 1
if k == img.size[0] * 3:
k = 0
print
cValue = coverData.next()
bitInfo = Helper.plainInfo2bitInfo(plainInfo)
k = 0
for bit in bitInfo.bit():
print "%s" % (bit),
k += 1
if k == 8:
k = 0
print
#-----------------------------------------------------------
def defaultTest():
"""Default tests."""
print "Testing steg module."
testDataStruct()
testHelper()
testEncode()
testDecode()
#-----------------------------------------------------------
def main():
"""Test steg module."""
defaultTest()
#-----------------------------------------------------------
if __name__ == "__main__":
main()
#-----------------------------------------------------------
And that is a basic framework for a Steganography module.
The object oriented design could be altered, perhaps in respect to the Helper class, that could be tied in more with the DataStruct class, but having the Helper separate should allow for code refactoring and to test new ideas out quickly.
Mild encryption is used in the module and embeded in the plainInfo encoding into the coverData, it is data dependant and of course the key can be changed. The length of the message is entered in a random position in the text, followed by a random non numeric character. This tends to further randomize the data to the right of the message length. In essence each resulting stegData image encoded with the same coverData and key will not always be the same, though of course the data derived will be identical.
The encoding itself happens backwards per character. This occurred because of how the bitInfo generator was written, but I liked the idea so kept it in place, it is just more obfuscation.
The obvious drawback to the encoding method used, is the data is at the head of the image and follows a very linear path. This of course is a common Steganography feature but it could be changed easily. The image generator function is the place to do it. The reason I did not want to change this will become clearer later. I wanted this module to be basic with a few clever touches.
Making the pattern a different type and incorporating different patterns is a core concept in Steganography. Using the plainInfo itself to generate the pattern is another smart move. The main reason for varying the pattern is because of Steganography Analysis (Steganalysis the art of cracking Steganography), people look to find original coverData and resulting stegData, once this is achieved it is quite trivial to spot the pattern used. If you always use the same pattern then it can compromise all your stegData previous and in the future. Ways around this is to noise all the stegData, so the message pattern is not obvious even with both the coverData and the stegData of one set.
With all this said, it has to be remembered that Steganography is not encryption, Steganography relies more on stealth, and there is good practice and bad practice in maintaining that stealth. Ensuring the coverData is destroyed after use is a good move, and generating the coverData uniquely is advisable. The coverData itself needs to be non uniform, photographs tend to work well, as does gimping the image a bit for effects, knowledge of how to work basic image transformations across sections is useful, but remember that each time you work on the coverData you could be leaving a trail back to it from which the stegData could be compared.
I introduced a gzip to the clearTxt before converting to plainInfo, this helps in reducing patterns, but does make a pattern of its own, namely the length of the zip itself stored at the front of the data. The message length is going to be added after, so the gzip would not decode with that in place. And, to further hide the fact the message could be a zipped message, the plainInfo is was cut in two and joined back together at opposite ends; this just furthers hides any patterns.
The above example of a Steganography library is not designed for speed. The design instead allows for extension and exploration. Feel free to add code to see the inner working in the various methods, and output the properties to see how they change. If you wish to incorporate any of this code into an application or module just give a cite to Poised Solutions Ltd in your code, and if you have a website a link to this site http://www.poisedsolutions.com/steganography/ or to the Poised Solutions site http://www.poisedsolutions.com would be appreciated.
To download the python source code and the example coverData PNG used in development click here to download.
The code runs through the default tests, but it may not work in every permutation of operation. I have tried to cover most of the bases of how data would flow into the structures to allow data to be injected at certain points, but I may have missed a few possibilities. I will be working with the code to produce a compiled application, so if I spot any glaring errors I will update this page and the downloads.
| Previous page - Interactive Example | Steganography Home | Next page - Steganalysis |