else/Steganographic/steganographic.py

279 lines
11 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 for the content length
HEADER_SIZE = 11
class StegError(Exception):
pass
def binary(i):
return bin(i)[2:].rjust(8, '0')
def increment_pixel(save=True):
'''
Increment the active channel, and roll to the next pixel when appropriate.
'''
global pixel
global pixel_index
global channel_index
channel_index += 1
if channel_index == 3:
channel_index = 0
if save:
image.putpixel((pixel_index % image.size[0], pixel_index // image.size[0]), tuple(pixel))
#print('wrote', pixel)
pixel_index += 1
pixel = list(image.getpixel( (pixel_index % image.size[0], pixel_index // image.size[0]) ))
#print('opend', pixel)
def bytes_to_pixels(bytes):
return ((bytes * (8 / 3)) + 14)
def pixels_to_bytes(pixels):
return ((3 * (pixels - 14)) / 8)
############## #### #### ######## ###### ########## ##############
#### ## #### #### #### #### #### #### #### #### #### ##
#### ###### #### #### #### #### #### #### #### ####
#### ## ######## #### #### #### #### #### #### #### ##
########## ############## #### #### #### #### #### ##########
#### ## #### ######## #### #### #### #### #### #### ##
#### #### ###### #### #### #### #### #### #### ####
#### ## #### #### #### #### #### #### #### #### #### ##
############## #### #### ######## ###### ########## ##############
def encode(imagefilename, secretfilename):
global image
global pixel
global pixel_index
global channel_index
pixel_index = 0
channel_index = 0
def modify_pixel(bit):
global pixel
global channel_index
#print(channel_index, bit)
#print(pixel_index, channel_index, bit)
channel = pixel[channel_index]
channel = binary(channel)[:7] + bit
channel = int(channel, 2)
pixel[channel_index] = channel
#print(pixel)
print('Hiding "%s" within "%s"' % (secretfilename, imagefilename))
image = Image.open(imagefilename)
totalpixels = image.size[0] * image.size[1]
if totalpixels < HEADER_SIZE:
raise StegError('Image cannot have fewer than %d pixels. They are used to store Secret\'s length' % HEADER_SIZE)
secretfile = open(secretfilename, 'rb')
secret = secretfile.read()
secret = list(secret)
if secret == []:
raise StegError('The Secret can\'t be 0 bytes.')
secret_extension = os.path.splitext(secretfilename)[1][1:]
secret_content_length = (len(secret)) + (len(secret_extension)) + 1
requiredpixels = math.ceil(((secret_content_length * 8) + 32) / 3)
if totalpixels < requiredpixels:
raise StegError('Image does not have enough pixels to store the Secret'
'Must have at least %d pixels' % requiredpixels)
print('%d available pixels, %d required' % (totalpixels, requiredpixels))
# --> YOU ARE HERE <--
pixel = list(image.getpixel((0, 0)))
# Write secret length
secret_content_length_b = binary(secret_content_length).rjust(32, '0')
for x in range(32):
modify_pixel(secret_content_length_b[x])
increment_pixel()
# Write the secret extension
for character in (secret_extension + chr(0)):
character = ord(character)
character = binary(character)
for bit in character:
modify_pixel(bit)
increment_pixel()
# Write the secret data
for (index, byte) in enumerate(secret):
if index % 1024 == 0:
percentage = (index + 1) / len(secret)
percentage = '%07.3f%%\r' % (100 * percentage)
print(percentage, end='')
# Convert byte integer to a binary string, and loop through characters
byte = binary(byte)
for (bindex, bit) in enumerate(byte):
modify_pixel(bit)
if not (index == secret_content_length -1 and bindex == 7):
# If your Image dimensions are at the extreme limit of the Secret size,
# this would otherwise raise IndexError as it tries to grab the next pixel
# off the canvas.
increment_pixel()
print('100.000%') # you know it
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)
image.putpixel((pixel_index % image.size[0], pixel_index // image.size[0]), tuple(pixel))
newname = os.path.splitext(imagefilename)[0]
newname = '%s (%s).png' % (newname, os.path.basename(secretfilename).replace('.', '_'))
print(newname)
image.save(newname)
########## ############## ######## ###### ########## ##############
#### #### #### ## #### #### #### #### #### #### #### ##
#### #### #### #### #### #### #### #### #### ####
#### #### #### ## #### #### #### #### #### #### ##
#### #### ########## #### #### #### #### #### ##########
#### #### #### ## #### #### #### #### #### #### ##
#### #### #### #### #### #### #### #### #### ####
#### #### #### ## #### #### #### #### #### #### #### ##
########## ############## ######## ###### ########## ##############
def decode(imagefilename):
global image
global pixel
global pixel_index
global channel_index
pixel_index = 0
channel_index = 0
print('Extracting content from "%s"' % imagefilename)
image = Image.open(imagefilename)
# determine the content length
content_length = ''
for x in range(11):
pixel = list(image.getpixel( (pixel_index % image.size[0], pixel_index // image.size[0]) ))
pixel = pixel[:3]
#print(pixel)
content_length += ''.join([bin(i)[-1] for i in pixel])
pixel_index += 1
content_length = content_length[:-1]
content_length = int(content_length, 2)
print('Content bytes:', content_length)
# Continue from the end of the header
# This would have been automatic if I used `increment_pixel`
pixel_index = 10
channel_index = 2
# determine secret extension
extension = ''
while extension[-8:] != '00000000' or len(extension) % 8 != 0:
channel = pixel[channel_index]
channel = binary(channel)
channel = channel[-1]
extension += channel
increment_pixel(save=False)
extension = extension[:-8]
extension = [extension[8*x: (8*x)+8] for x in range(len(extension)//8)]
extension = [chr(int(x, 2)) for x in extension]
extension = ''.join(extension)
print('Extension:', extension)
# Remove the extension length
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
for byte in range(content_length):
if byte % 1024 == 0:
percentage = (byte + 1) / content_length
percentage = '%07.3f%%\r' % (100 * percentage)
print(percentage, end='')
activebyte = ''
for bit in range(8):
channel = pixel[channel_index]
channel = binary(channel)[-1]
activebyte += channel
if not (byte == content_length - 1 and bit == 7):
# If your Image dimensions are at the extreme limit of the Secret size,
# this would otherwise raise IndexError as it tries to grab the next pixel
# off the canvas.
increment_pixel(save=False)
activebyte = '%02x' % int(activebyte, 2)
outfile.write(binascii.a2b_hex(activebyte))
print('100.000%') # I'm on fire
print(newname)
outfile.close()
if __name__ == '__main__':
if (len(sys.argv) == 1) or (sys.argv[1] not in ['encode', 'decode']):
print('Usage:')
print('> 3bitspixel.py encode imagefilename.png secretfilename.ext')
print('> 3bitspixel.py decode lacedimagename.png')
imagefilename = sys.argv[2]
if sys.argv[1] == 'encode':
secretfilename = sys.argv[3]
encode(imagefilename, secretfilename)
else:
decode(imagefilename)