-
Notifications
You must be signed in to change notification settings - Fork 0
/
LeastSignificantBit.py
338 lines (245 loc) · 8.41 KB
/
LeastSignificantBit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#!/usr/bin/python3
"""Python program to implement least significant bit steganography"""
import pygame, sys, getopt
from pygame.locals import *
from typing import Dict, List, IO
from io import FileIO
import os.path
def main(args: List) -> None:
"""
Main entry point to the program
Arguments:
args -- arugments passed in from command line
"""
if len(args) < 2:
print_usage()
return
try:
opts, args = getopt.getopt(args, 'n:')
opts = dict(opts)
if len(args) == 3:
#Retrieve the necessary arguments from the commandline
message_file = args[0]
input_image = args[1]
output_filename = args[2]
run_steganography(message_file, input_image, output_filename, opts)
elif len(args) == 2:
input_image = args[0]
output_filename = args[1]
decrypt_image(input_image, output_filename, opts)
except getopt.GetoptError:
print_usage()
def print_usage():
"""
Prints the usage of the tool to the console
"""
print_version()
print()
print('Usage: ')
print('\tpython[3] LeastSignificantBit.py [-n 2] [options] <input_message_file> <input_image> <output_filename>')
print('\tpython[3] LeastSignificantBit.py [-n 2] [options] <input_image> <output_filename>')
print('Options: ')
print('\t-n\tThe number of bits to modify in each pixel\'s channel')
def print_version():
"""
Prints the name and version of the program
"""
try:
version_file = open('version.txt', 'r')
version = version_file.readline();
version_file.close()
print('Least Significant Bit Steganography')
print('By Sam Faulkner')
print("version: " + str(version))
except:
pass
def decrypt_image(input_image_filename: str, output_filename: str, opts: Dict) -> None:
"""
Function that calls and/or performs the necessary functions to retrieve a message from
an image
Arguments:
input_image_filename -- the filepath of the image that has a secret message
output_filename -- the filepath of where they want the image saved
"""
image = retrieve_image(input_image_filename)
output_file = get_output_file(output_filename)
if not image or not output_file:
return
decrypt_message(image, output_file, int(opts['-n']))
def decrypt_message(image: pygame.Surface, output_file: IO[str], num_bits: int) -> None:
"""
Function that looks into the image and retrieves the hidden message
Arguments:
image -- the pygame.Surface object holding the data
output_file -- the writeable stream that we'll write the data to
"""
write_buffer = bytes()
image_buffer = image.get_view('1')
image_bytes = image_buffer.raw
current_byte_index = 0
current_bits = 0
bit_mask = construct_mask(num_bits)
done = False
while not done and current_byte_index < len(image_bytes):
for i in range(8):
# because each value will only be a single byte, it doesn't matter what the byte order is
retrieved_value = image_bytes[current_byte_index] & bit_mask
current_bits = current_bits | (retrieved_value << (i * num_bits))
current_byte_index += 1
for i in range(num_bits):
current_value = (current_bits & (0xFF << (i * 8))) >> (i * 8)
current_byte = bytes([current_value])
# Check for EOT character
if (current_byte == b'\xFF'):
done = True
break
write_buffer += current_byte
current_bits = 0
if output_file.write(write_buffer) != len(write_buffer):
raise IOError('Unable to write the message to file')
else:
output_file.flush()
write_buffer = bytes()
output_file.flush()
output_file.close()
def get_output_file(filepath: str) -> IO[str]:
"""
Function that returns a writable file stream from the given
filename
Arugments:
filepath -- the filepath of where the output should go
"""
file = None
try:
file = FileIO(filepath, 'w')
except (IOError, OSError) as e:
print(e)
return file
def run_steganography(message_filename: str, input_image_filename: str, output_filename: str, opts: Dict) -> None:
"""
Function that calls and/or performs the main steganographic function of the program
Arguments:
message_filename -- filename of the user's desired hidden message
input_image_filename -- filename of the user's png image they want to embed their message in
output_filename -- desired file the user wants to save their result to
opts -- dictionary containing desired options
"""
message_file = retrieve_message_file(message_filename)
image = retrieve_image(input_image_filename)
# At this point error messages have already printed out to the user
if not image or not message_file:
return
if not verify_filename(output_filename):
return
try:
embed_image(message_file, image, int(opts['-n']))
except Exception as e:
print(e)
pygame.image.save(image, output_filename)
message_file.close()
def verify_filename(filename: str) -> bool:
"""
Verifies that the given filename is appropriate for
saving a PNG image to
Arguments:
filename -- the filepath that requires verification
"""
root, ext = os.path.splitext(filename)
# The only verification right now is checking for file extension,
# as pygame relies on it being the correct kind
if ext != '.png':
return False
return True
def retrieve_message_file(message_filename: str) -> IO[str]:
"""
Retrieves the file the user wants to use as his/her message
Arguments:
message_filename -- The filepath of where the message file is located
"""
message_file = None
try:
message_file = FileIO(message_filename)
except (IOError, OSError) as e:
print(e)
return message_file
def retrieve_image(input_image_filename: str) -> pygame.Surface:
"""
Retrieves the file the user wants to use as their image, then
uses pygame to load it into a usable data structure (pygame.Surface)
Arguments:
input_image_filename -- the filepath of the image file is located
"""
image = None
try:
root, ext = os.path.splitext(input_image_filename)
if ext != '.png':
raise IOError('\'<input_image>\' is not a .png file')
# This checks if pygame can load extended image formats, which is necessary for PNGs
if not pygame.image.get_extended():
raise Error('pygame is not able to load .png images')
image = pygame.image.load(input_image_filename)
except (IOError, OSError) as e:
print('invalid \'<input_image>\' argument')
print(e)
except Exception as e:
print(e)
return image
def construct_mask(length: int) -> int:
"""
Constructs a variable length bit mask
Arguments:
length -- the length of the desired mask
"""
mask = '1' * length
return int(mask, 2)
def construct_inverted_mask(length: int) -> int:
"""
Constructs a variable length bit mask with the
intent that the mask can "mask off" certain values
instead of retrieving them
Arguments:
length -- the length of the desired mask
"""
mask = '1' * (8 - length)
mask += '0' * length
return int(mask, 2)
def embed_image(message: IO[str], image: pygame.Surface, num_bits_modify: int) -> None:
"""
Mutates the given image to embed the given message within it
Arguments:
message -- the file object containing the wanted message
image -- the pygame Surface with pixel data
num_bits_modify -- the number of bits to modify in each pixel value
"""
if (num_bits_modify > 8):
raise Error('No more than 8 bits per pixel\'s channel')
elif (num_bits_modify <= 0):
raise Error('Must modify at least 1 bit per pixel channel')
image.lock()
image_buffer = image.get_view('1')
image_bytes = image_buffer.raw
image_bytes_length = len(image_bytes)
current_byte_index = 0
stride_size = 8 * num_bits_modify
bit_mask = construct_mask(num_bits_modify)
inverted_mask = construct_inverted_mask(num_bits_modify)
current_message_stream = message.read(stride_size)
while len(current_message_stream) > 0:
if len(current_message_stream) < stride_size:
current_message_stream += b'\xFF'
# Grab the value from the message
bits = int.from_bytes(current_message_stream[::-1], byteorder='big')
i = 0
while (i * num_bits_modify) < (len(current_message_stream) * 8) + ((len(current_message_stream) * 8) % num_bits_modify):
value_to_embed = (bits & (bit_mask << (num_bits_modify * i))) >> (num_bits_modify * i)
current_image_byte = image_bytes[current_byte_index]
current_image_byte = (current_image_byte & inverted_mask) | value_to_embed
image_buffer.write(bytes([current_image_byte]), current_byte_index)
i += 1
current_byte_index += 1
if current_byte_index >= len(image_bytes):
raise Exception('Message too big for image')
current_message_stream = message.read(stride_size)
image.unlock()
if __name__ == '__main__':
main(sys.argv[1:])