358 lines
No EOL
14 KiB
Python
358 lines
No EOL
14 KiB
Python
'''
|
|
For each color channel of each pixel in an Image, modify the least significant bit to represent a bit of the Secret file.
|
|
This changes the RGB value of the pixel by a usually-imperceptible amount.
|
|
The first 32 bits (10.66 pixels) will be used to store the length of the Secret content in big endian.
|
|
Then, the Secret's extension is stored. A null byte indicates the end of the extension. This section is of variable length.
|
|
A file with no extension requires only that null byte. A file with an extension requires 1 additional byte per character.
|
|
|
|
Smallest image possible = 16 pixels with 48 bit secret: 32 for header; 8 for null extension; 8 for data.
|
|
Each Image pixel holds 3 Secret bits, so the Image must have at least ((secretbytes * (8 / 3)) + 14) pixels.
|
|
An Image can hold ((3 * (pixels - 14)) / 8) Secret bytes.
|
|
|
|
Usage:
|
|
> steganographic.py encode imagefilename.png secretfilename.ext
|
|
> steganographic.py decode lacedimagename.png
|
|
|
|
|
|
Reference table for files with NO EXTENSION.
|
|
For each extension character, subtract 1 byte from secret size
|
|
|
|
pixels | example dimensions | Secret file size
|
|
100 | 10 x 10 | 32 bytes
|
|
400 | 20 x 20 | 144 bytes
|
|
2,500 | 50 x 50 | 932 bytes
|
|
10,000 | 100 x 100 | 3,744 bytes
|
|
40,000 | 200 x 200 | 14,994 bytes
|
|
25,000 | 500 x 500 | 93,744 bytes (91 kb)
|
|
1,000,000 | 1,000 x 1,000 | 374,994 bytes (366 kb)
|
|
4,000,000 | 2,000 x 2,000 | 1,499,994 bytes (1.43 mb)
|
|
25,000,000 | 5,000 x 5,000 | 9,374,994 bytes (8.94 mb)
|
|
100,000,000 | 10,000 x 10,000 | 37,499,994 bytes (35.7 mb)
|
|
|
|
pixels | example dimensions | Secret file size
|
|
100 | 10 x 10 | 32 bytes
|
|
697 | 25 x 28 (700) | 256 bytes
|
|
2,745 | 50 x 55 (2,750) | 1,024 bytes (1 kb)
|
|
21,860 | 142 x 154 (21,868) | 8,192 bytes (8 kb)
|
|
87,396 | 230 x 380 (87,400) | 32,768 bytes (32 kb)
|
|
349,540 | 463 x 755 (349,565) | 131,072 bytes (128 kb)
|
|
1,398,116 | 1146 x 1120 (1,398,120) | 524,288 bytes (512 kb)
|
|
2,796,217 | 1621 x 1725 (2,796,225) | 1,048,576 bytes (1 mb)
|
|
11,184,825 | 3500 x 3200 (11,200,000) | 4,194,304 bytes (4 mb)
|
|
44,739,257 | 6700 x 6700 (44,890,000) | 16,777,216 bytes (16 mb)
|
|
89,478,500 | 9500 x 9500 (90,250,000) | 33,554,432 bytes (32 mb)
|
|
|
|
'''
|
|
from PIL import Image
|
|
import binascii
|
|
import math
|
|
import os
|
|
import sys
|
|
|
|
# 11 pixels for the secret file size
|
|
HEADER_SIZE = 11
|
|
|
|
FILE_READ_SIZE = 4 * 1024
|
|
|
|
class StegError(Exception):
|
|
pass
|
|
|
|
class BitsToImage:
|
|
def __init__(self, image, bitness):
|
|
self.image = image
|
|
self.bitness = bitness
|
|
self.width = image.size[0]
|
|
self.pixel_index = -1
|
|
self.channel_index = 0
|
|
self.bit_index = self.bitness - 1
|
|
self.active_pixel = None
|
|
self.x = 0
|
|
self.y = 0
|
|
|
|
def _write(self, bit):
|
|
if self.active_pixel is None:
|
|
self.pixel_index += 1
|
|
self.channel_index = 0
|
|
self.bit_index = self.bitness - 1
|
|
(self.x, self.y) = index_to_xy(self.pixel_index, self.width)
|
|
self.active_pixel = list(self.image.getpixel((self.x, self.y)))
|
|
|
|
channel = self.active_pixel[self.channel_index]
|
|
channel = set_bit(channel, self.bit_index, int(bit))
|
|
self.active_pixel[self.channel_index] = channel
|
|
self.bit_index -= 1
|
|
|
|
if self.bit_index < 0:
|
|
# We have exhausted our bitness for this channel.
|
|
self.bit_index = self.bitness - 1
|
|
self.channel_index += 1
|
|
if self.channel_index == 3:
|
|
# We have exhausted the channels for this pixel.
|
|
self.image.putpixel((self.x, self.y), tuple(self.active_pixel))
|
|
self.active_pixel = None
|
|
|
|
def write(self, bits):
|
|
for bit in bits:
|
|
self._write(bit)
|
|
|
|
|
|
class ImageToBits:
|
|
def __init__(self, image, bitness):
|
|
self.image = image
|
|
self.bitness = bitness
|
|
self.width = image.size[0]
|
|
self.height = image.size[1]
|
|
self.pixel_index = -1
|
|
self.bit_index = bitness
|
|
self.active_byte = []
|
|
self.pixels = self.image.getdata()
|
|
#self.bits = ''
|
|
#for pixel in self.pixels:
|
|
# for channel in pixel:
|
|
# self.bits += binary(channel)[-bitness:]
|
|
#print(len(self.bits))
|
|
|
|
|
|
def _read(self):
|
|
if len(self.active_byte) == 0:
|
|
self.pixel_index += 1
|
|
self.active_byte = self.pixels[self.pixel_index]
|
|
self.active_byte = self.active_byte[:3]
|
|
self.active_byte = [binary(channel) for channel in self.active_byte]
|
|
self.active_byte = [channel[-bitness:] for channel in self.active_byte]
|
|
self.active_byte = ''.join(self.active_byte)
|
|
self.active_byte = list(self.active_byte)
|
|
|
|
ret = self.active_byte.pop(0)
|
|
self.bit_index += 1
|
|
return ret
|
|
|
|
def read(self, bits=1):
|
|
return ''.join(self._read() for x in range(bits))
|
|
|
|
|
|
def binary(i):
|
|
return bin(i)[2:].rjust(8, '0')
|
|
|
|
def chunk_iterable(iterable, chunk_length, allow_incomplete=True):
|
|
'''
|
|
Given an iterable, divide it into chunks of length `chunk_length`.
|
|
If `allow_incomplete` is True, the final element of the returned list may be shorter
|
|
than `chunk_length`. If it is False, those items are discarded.
|
|
'''
|
|
if len(iterable) % chunk_length != 0 and allow_incomplete:
|
|
overflow = 1
|
|
else:
|
|
overflow = 0
|
|
|
|
steps = (len(iterable) // chunk_length) + overflow
|
|
return [iterable[chunk_length * x : (chunk_length * x) + chunk_length] for x in range(steps)]
|
|
|
|
def index_to_xy(index, width):
|
|
x = index % width
|
|
y = index // width
|
|
return (x, y)
|
|
|
|
def bytes_to_pixels(bytes):
|
|
return ((bytes * (8 / 3)) + 14)
|
|
|
|
def pixels_to_bytes(pixels):
|
|
return ((3 * (pixels - 14)) / 8)
|
|
|
|
def set_bit(number, index, newvalue):
|
|
# Thanks unwind
|
|
# http://stackoverflow.com/a/12174051/5430534
|
|
mask = 1 << index
|
|
number &= ~mask
|
|
if newvalue:
|
|
number |= mask
|
|
return number
|
|
|
|
############## #### #### ######## ###### ########## ##############
|
|
#### ## #### #### #### #### #### #### #### #### #### ##
|
|
#### ###### #### #### #### #### #### #### #### ####
|
|
#### ## ######## #### #### #### #### #### #### #### ##
|
|
########## ############## #### #### #### #### #### ##########
|
|
#### ## #### ######## #### #### #### #### #### #### ##
|
|
#### #### ###### #### #### #### #### #### #### ####
|
|
#### ## #### #### #### #### #### #### #### #### #### ##
|
|
############## #### #### ######## ###### ########## ##############
|
|
def encode(imagefilename, secretfilename, bitness=1):
|
|
pixel_index = 0
|
|
channel_index = 0
|
|
|
|
if bitness < 1:
|
|
raise ValueError('Cannot modify less than 1 bit per channel')
|
|
if bitness > 8:
|
|
raise ValueError('Cannot modify more than 8 bits per channel')
|
|
|
|
print('Hiding "%s" within "%s"' % (secretfilename, imagefilename))
|
|
secret_size = os.path.getsize(secretfilename)
|
|
if secret_size == 0:
|
|
raise StegError('The Secret can\'t be 0 bytes.')
|
|
|
|
image = Image.open(imagefilename)
|
|
image_steg = BitsToImage(image, bitness)
|
|
|
|
total_pixels = image.size[0] * image.size[1]
|
|
if total_pixels < HEADER_SIZE:
|
|
raise StegError('Image cannot have fewer than %d pixels. They are used to store Secret\'s length' % HEADER_SIZE)
|
|
|
|
secret_extension = os.path.splitext(secretfilename)[1][1:]
|
|
secret_content_length = (secret_size) + (len(secret_extension)) + 1
|
|
requiredpixels = math.ceil(((secret_content_length * 8) + 32) / (3 * bitness))
|
|
if total_pixels < requiredpixels:
|
|
raise StegError('Image does not have enough pixels to store the Secret. '
|
|
'Must have at least %d pixels' % requiredpixels)
|
|
|
|
print('%d pixels available, %d required' % (total_pixels, requiredpixels))
|
|
|
|
# --> YOU ARE HERE <--
|
|
|
|
pixel = list(image.getpixel((0, 0)))
|
|
|
|
# Write secret length
|
|
secret_content_length_b = binary(secret_content_length).rjust(32, '0')
|
|
print('Content bytes:', secret_content_length)
|
|
image_steg.write(secret_content_length_b)
|
|
|
|
# Write the secret extension
|
|
for character in (secret_extension + chr(0)):
|
|
image_steg.write(binary(ord(character)))
|
|
|
|
# Write the secret data
|
|
bytes_written = 0
|
|
one_complete_file = False
|
|
|
|
# Yes, we're assuming the whole file fits in memory.
|
|
secret_file = open(secretfilename, 'rb')
|
|
secret_data = secret_file.read()
|
|
secret_data_size = len(secret_data)
|
|
secret_index = 0
|
|
for pixel_number in range(total_pixels):
|
|
if pixel_number % 4 == 0:
|
|
#percentage = (bytes_written + 1) / secret_size
|
|
#percentage = '%07.3f%%\r' % (100 * percentage)
|
|
#print(percentage, end='')
|
|
print(pixel_number)
|
|
|
|
secret_chunk = secret_data[secret_index:secret_index+FILE_READ_SIZE]
|
|
secret_index += FILE_READ_SIZE
|
|
if len(secret_chunk) < FILE_READ_SIZE:
|
|
one_complete_file = True
|
|
#secret_index = 0
|
|
rs = FILE_READ_SIZE - len(secret_chunk)
|
|
secret_chunk += os.urandom(rs)
|
|
#secret_chunk += secret_data[secret_index:secret_index+rs]
|
|
|
|
secret_chunk = list(secret_chunk)
|
|
secret_chunk = [binary(byte) for byte in secret_chunk]
|
|
bytes_written += len(secret_chunk)
|
|
secret_chunk = ''.join(secret_chunk)
|
|
try:
|
|
image_steg.write(secret_chunk)
|
|
except IndexError:
|
|
if one_complete_file:
|
|
break
|
|
else:
|
|
raise
|
|
|
|
# haha
|
|
print('100.000%')
|
|
|
|
if channel_index != 0:
|
|
# The Secret data has finished, but we still have an unsaved pixel
|
|
# (because channel_index is set to 0 when we save the active pixel above)
|
|
(x, y) = index_to_xy(pixel_index, image.size[0])
|
|
image.putpixel((x, y), tuple(pixel))
|
|
|
|
new_name = os.path.splitext(imagefilename)[0]
|
|
original_name = os.path.basename(secretfilename).replace('.', '_')
|
|
newname = '%s (%s) (%d).png' % (new_name, original_name, bitness)
|
|
print('Writing:', newname)
|
|
image.save(newname)
|
|
|
|
|
|
|
|
########## ############## ######## ###### ########## ##############
|
|
#### #### #### ## #### #### #### #### #### #### #### ##
|
|
#### #### #### #### #### #### #### #### #### ####
|
|
#### #### #### ## #### #### #### #### #### #### ##
|
|
#### #### ########## #### #### #### #### #### ##########
|
|
#### #### #### ## #### #### #### #### #### #### ##
|
|
#### #### #### #### #### #### #### #### #### ####
|
|
#### #### #### ## #### #### #### #### #### #### #### ##
|
|
########## ############## ######## ###### ########## ##############
|
|
def decode(imagefilename, bitness=1):
|
|
print('Extracting content from "%s"' % imagefilename)
|
|
image = Image.open(imagefilename)
|
|
image_steg = ImageToBits(image, bitness)
|
|
|
|
# determine the content length
|
|
content_length = image_steg.read(32)
|
|
content_length = int(content_length, 2)
|
|
print('Content bytes:', content_length)
|
|
|
|
# determine secret extension
|
|
extension = ''
|
|
while extension[-8:] != '00000000' or len(extension) % 8 != 0:
|
|
extension += image_steg.read()
|
|
|
|
extension = chunk_iterable(extension, 8)
|
|
extension.remove('00000000')
|
|
extension = [chr(int(x, 2)) for x in extension]
|
|
extension = ''.join(extension)
|
|
print('Extension:', extension)
|
|
|
|
# Remove the extension length, and null byte
|
|
content_length -= 1
|
|
content_length -= len(extension)
|
|
|
|
# Prepare writes
|
|
newname = os.path.splitext(imagefilename)[0]
|
|
if extension != '':
|
|
extension = '.' + extension
|
|
newname = '%s (extracted)%s' % (newname, extension)
|
|
outfile = open(newname, 'wb')
|
|
|
|
# extract data
|
|
bytes_written = 0
|
|
while bytes_written < content_length:
|
|
if bytes_written % 1024 == 0:
|
|
percentage = (bytes_written + 1) / content_length
|
|
percentage = '%07.3f%%\r' % (100 * percentage)
|
|
print(percentage, end='')
|
|
|
|
byte = image_steg.read(8)
|
|
byte = '%02x' % int(byte, 2)
|
|
outfile.write(binascii.a2b_hex(byte))
|
|
bytes_written += 1
|
|
|
|
# I'm on fire
|
|
print('100.000%')
|
|
print('Wrote', newname)
|
|
outfile.close()
|
|
|
|
def listget(li, index, fallback=None):
|
|
try:
|
|
return li[index]
|
|
except IndexError:
|
|
return fallback
|
|
|
|
if __name__ == '__main__':
|
|
command = listget(sys.argv, 1, '').lower()
|
|
if command not in ['encode', 'decode']:
|
|
print('Usage:')
|
|
print('> steganographic.py encode imagefilename.png secretfilename.ext bitness')
|
|
print('> steganographic.py decode lacedimagename.png bitness')
|
|
quit()
|
|
|
|
imagefilename = sys.argv[2]
|
|
|
|
if command == 'encode':
|
|
secretfilename = sys.argv[3]
|
|
bitness = int(sys.argv[4])
|
|
encode(imagefilename, secretfilename, bitness)
|
|
else:
|
|
bitness = int(sys.argv[3])
|
|
decode(imagefilename, bitness) |