stepic 0.3 uses the simplest approach to steganography in images. Quoting directly from the module:
def decode_imdata(imdata):
'''Given a sequence of pixels, returns an iterator of characters
encoded in the image'''
imdata = iter(imdata)
while True:
pixels = list(imdata.next()[:3] + imdata.next()[:3] + imdata.next()[:3])
byte = 0
for c in xrange(7):
byte |= pixels[c] & 1
byte <<= 1
byte |= pixels[7] & 1
yield chr(byte)
if pixels[-1] & 1:
break
Every octet of the secret data, plus a flag whether it's the last byte, is hidden in three consecutive pixels. To be more precise, stepic uses the least significant bits of the first three components (often RGB) of each pixel. See this very ugly diagram, for an RGBA stream with 4 bits per component (D
means data, E
means end-of-stream):
| pixel 0 | pixel 1 | pixel 2 |
image viewer sees: | rrrr gggg bbbb aaaa | rrrr gggg bbbb aaaa | rrrr gggg bbbb aaaa |
stepic sees: | ___D ___D ___D ____ | ___D ___D ___D ____ | ___D ___D ___E ____ |
Because the noise introduced by this change is small in already "noisy" images (one 256th), you often can't really detect this visually. That means the goal of this technique is achieved: the data is hidden in plain sight, because nobody can distinguish it from naturally occurring noise.
This works. At least, it works for lossless formats, such as PNG. Alas, JPG is not lossless, and its compression will very likely change at least one of the encoded bits. It suffices to change the ninth bit to render this method pretty useless, since then the hidden data will be truncated to a single byte.
Steganography in JPG images is still possible, in many forms, but you can't really just tweak the decoded pixel values. A better (but more complicated) method might be to hide data in the parameters estimated by the compressor.