Advertisement
cmiN

tii

Jun 26th, 2012
156
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.89 KB | None | 0 0
  1. #! /usr/bin/env python
  2. # Text In Image
  3. # 02.01.2012 cmiN
  4. #
  5. # This is a simple GUI script which can hide text in pictures
  6. # using least significant bit method.
  7. # Also the input text can be encrypted and the output can be decrypted too
  8. # with a symmetric key using AES.
  9. # Writing is done directly on input image so be careful with certain extensions
  10. # because the output will always have the BMP format.
  11. #
  12. # Contact: cmin764@yahoo/gmail.com
  13.  
  14.  
  15. from Tkinter import * # widgets's classes
  16. from tkFileDialog import askopenfilename # get file name
  17. from tkMessageBox import showerror, showinfo # user dialog
  18. from PIL import Image # image converting
  19. from Crypto.Cipher import AES # text cipher
  20.  
  21.  
  22. class Engine:
  23.     """
  24.    Code for processing the image.
  25.    Separated from GUI.
  26.    """
  27.    
  28.     def __init__(self):
  29.         """ Initialize parameters. """
  30.         self.ext = "bmp" # save format
  31.         self.name = None # save name
  32.         self.path = None # save path
  33.         self.im = None # image object, read and write
  34.         self.generator = None # get locations to write/read bits
  35.         self.useAES = None # use it or not
  36.         self.aes = None # AES object
  37.         self.data = None # data to be written to image
  38.         self.width = None # image width
  39.         self.height = None # image height
  40.         self.tmp = None # last string, used when key changes
  41.        
  42.     def binary(self, nr, size):
  43.         """ Get 1&0 representation. """
  44.         return bin(nr).replace("0b", "").zfill(size * 8)
  45.        
  46.     def path_name(self, path):
  47.         """ Split a file path in path and name. """
  48.         ind = path.rfind("/") + 1
  49.         return (path[:ind], path[ind:])
  50.        
  51.     def set_generator(self):
  52.         """ Useful for resetting. """
  53.         self.generator = ((wp, hp, ch) for wp in xrange(self.width) # WxHxC
  54.                                        for hp in xrange(self.height)
  55.                                        for ch in xrange(3))
  56.    
  57.     def load(self, path):
  58.         """ Load image. """
  59.         self.im = Image.open(path)
  60.         (self.width, self.height) = self.im.size
  61.         (self.path, self.name) = self.path_name(path)
  62.         return self.width * self.height * 3 # total useful bytes
  63.        
  64.     def parse_key(self, key):
  65.         """ If key exists make an AES object from it. """
  66.         if not key:
  67.             self.aes = None # empty key == no encryption
  68.             return self.parse_string(self.tmp) # must return size (see the next return)
  69.         key.decode() # test availability
  70.         size = len(key)
  71.         for padding in (16, 24, 32): # fixed key size
  72.             if size <= padding:
  73.                 break
  74.         key += chr(0) * (padding - size)
  75.         self.aes = AES.new(key)
  76.         return self.parse_string(self.tmp) # if key changes you must update string
  77.        
  78.     def parse_string(self, string):
  79.         """ Convert to bitstring. """
  80.         if not string: # without string can't start the process
  81.             self.tmp = None
  82.             self.data = None
  83.             return 0
  84.         string.decode() # test availability
  85.         self.tmp = string
  86.         if self.useAES and self.aes: # encrypt it
  87.             string += chr(0) * ((16 - len(string) % 16) % 16) # multiple of 16 string
  88.             string = self.aes.encrypt(string)
  89.         string = str().join([self.binary(ord(x), 1) for x in string]) # convert every char in a set of 8 bits
  90.         size = self.binary(len(string), 4) # get binary representation of string's length in 4 bytes
  91.         self.data = size + string
  92.         return len(self.data)
  93.            
  94.     def write(self):
  95.         """ Write using LSB. """
  96.         self.set_generator() # rearm
  97.         for bit in self.data:
  98.             (wp, hp, ch) = self.generator.next() # get next position
  99.             values = list(self.im.getpixel((wp, hp))) # retrieve its values
  100.             tmp = self.binary(values[ch], 1) # convert one of them
  101.             values[ch] = int(tmp[:7] + bit, 2) # alter that channel
  102.             self.im.putpixel((wp, hp), tuple(values)) # put it back
  103.         self.im.save(self.path + self.name, format=self.ext) # save the new output
  104.        
  105.     def read(self):
  106.         """ Read from every LSB. """
  107.         self.set_generator() # rearm
  108.         total = self.width * self.height * 3
  109.         if total < 32:
  110.             raise Exception("Text not found.")
  111.         size = chunk = string = str()
  112.         i = 0 # for(i=0; true; ++i)
  113.         while True:
  114.             (wp, hp, ch) = self.generator.next() # i byte
  115.             values = self.im.getpixel((wp, hp))
  116.             tmp = self.binary(values[ch], 1)
  117.             if i < 32: # it's lame but I prefer string/bitset
  118.                 size += tmp[7]
  119.                 if i == 31:
  120.                     size = int(size, 2)
  121.                     if size < 1 or (size + 32) > total:
  122.                         raise Exception("Text not found.")
  123.             elif i < size + 32:
  124.                 chunk += tmp[7]
  125.                 if len(chunk) == 8:
  126.                     string += chr(int(chunk, 2))
  127.                     chunk = str()
  128.             else:
  129.                 break
  130.             i += 1
  131.         if self.useAES and self.aes:
  132.             if len(string) % 16 != 0:
  133.                 raise Exception("Text not encrypted.")
  134.             string = self.aes.decrypt(string).rstrip(chr(0))
  135.         string.decode() # rise an exception if invalid
  136.         return string
  137.                
  138.  
  139. class GUI(Frame):
  140.     """
  141.    Main window, inherited from Frame.
  142.    Here we put our widgets and set their behavior.
  143.    """
  144.    
  145.     def __init__(self, master=None, margin=30):
  146.         """ Same as Frame's constructor. """
  147.         Frame.__init__(self, master, padx=margin, pady=margin)
  148.         self.grid()
  149.         self.widgets()
  150.         self.behavior()
  151.        
  152.     def widgets(self):
  153.         """ Build and grid widgets. """
  154.         # ---- create variables ----
  155.         self.totalBytes = IntVar() # depends on image size
  156.         self.usedBytes = IntVar() # how many of them are used
  157.         self.textStatus = StringVar() # used per total bytes
  158.         self.useEncryption = IntVar() # 0-plain 1-AES
  159.         self.mode = IntVar() # 0-read 1-write
  160.         self.textOpt = dict() # text last config
  161.         self.keyOpt = dict() # key last config
  162.         self.loaded = False # image loaded or not
  163.         # ---- create widgets ----
  164.         self.label = Label(self, textvariable=self.textStatus)
  165.         self.about = Label(self, text="About", fg="blue")
  166.         self.text = Text(self, width=30, height=5, fg="grey")
  167.         self.scrollbar = Scrollbar(self, orient="vertical", command=self.text.yview)
  168.         self.loadButton = Button(self, text="Load", width=5, command=lambda: self.action("load"))
  169.         self.readRadio = Radiobutton(self, text="Read", variable=self.mode, value=0, command=self.set_state)
  170.         self.checkButton = Checkbutton(self, text="Use AES", variable=self.useEncryption, onvalue=1, offvalue=0, command=self.set_state)
  171.         self.startButton = Button(self, text="Start", width=5, state="disabled", command=lambda: self.action("start"))
  172.         self.writeRadio = Radiobutton(self, text="Write", variable=self.mode, value=1, command=self.set_state)
  173.         self.keyEntry = Entry(self, width=10, fg="grey", show="*")
  174.         # ---- show widgets ----
  175.         self.label.grid(row=0, column=0, columnspan=2, sticky="w")
  176.         self.about.grid(row=0, column=2, sticky="e")
  177.         self.text.grid(row=1, column=0, rowspan=3, columnspan=3)
  178.         self.scrollbar.grid(row=1, column=3, rowspan=3, sticky="ns")
  179.         self.loadButton.grid(row=4, column=0, sticky="w", pady=5)
  180.         self.readRadio.grid(row=4, column=1)
  181.         self.checkButton.grid(row=4, column=2, sticky="e")
  182.         self.startButton.grid(row=5, column=0, sticky="w")
  183.         self.writeRadio.grid(row=5, column=1)
  184.         self.keyEntry.grid(row=5, column=2, sticky="e")
  185.        
  186.     def behavior(self):
  187.         """ Customize widgets. """
  188.         self.text.config(yscrollcommand=self.scrollbar.set)
  189.         self.text.insert(0.0, "Text here")
  190.         self.keyEntry.insert(0, "Key here")
  191.         self.text.bind("<Button>", self.handle_event)
  192.         self.text.bind("<KeyRelease>", self.handle_event)
  193.         self.keyEntry.bind("<Button>", self.handle_event)
  194.         self.keyEntry.bind("<KeyRelease>", self.handle_event)
  195.         self.textOpt = self.get_opt(self.text)
  196.         self.keyOpt = self.get_opt(self.keyEntry)
  197.         self.about.bind("<Button>", self.handle_event)
  198.         self.set_state()
  199.        
  200.     def action(self, arg):
  201.         """ What every button triggers. """
  202.         if arg == "load":
  203.             fileTypes = [("BMP", "*.bmp"), ("JPEG", ("*.jpeg", "*.jpg")), ("PNG", "*.png"), ("All Files", "*.*")]
  204.             path = askopenfilename(parent=self, title="Open image", filetypes=fileTypes)
  205.             if path != "":
  206.                 try:
  207.                     self.totalBytes.set(app.load(path))
  208.                 except IOError as msg:
  209.                     showerror("Error", str(msg).capitalize().strip(".") + ".") # some formatting
  210.                 else:
  211.                     self.loaded = True
  212.                     self.set_state()
  213.                     self.master.title("Text In Image - %s" % app.name) # update name in title
  214.         elif arg == "start":
  215.             if self.mode.get():
  216.                 try:
  217.                     app.write()
  218.                 except Exception as msg:
  219.                     showerror("Error", str(msg).capitalize().strip(".") + ".")
  220.                 else:
  221.                     showinfo("Info", "Done.")
  222.             else:
  223.                 try:
  224.                     string = app.read()
  225.                 except UnicodeError:
  226.                     showerror("Error", "Text not found or wrong key.")
  227.                 except Exception as msg:
  228.                     showerror("Error", str(msg).capitalize().strip(".") + ".")
  229.                 else:
  230.                     self.text.config(state="normal")
  231.                     self.textOpt["fg"] = "black" # touched
  232.                     self.text.delete(0.0, END)
  233.                     self.text.insert(0.0, string)
  234.                     self.text.config(state="disabled")
  235.                     self.usedBytes.set(app.parse_string(string))
  236.                     self.set_status()
  237.                     showinfo("Info", "Done.")
  238.                    
  239.     def set_status(self):
  240.         """ Get used per total bytes. """
  241.         string = "%9.3f%s/%9.3f%s"
  242.         unit1 = unit2 = "b"
  243.         used = self.usedBytes.get()
  244.         total = self.totalBytes.get()
  245.         if used > total:
  246.             self.label.config(fg="red")
  247.         else:
  248.             self.label.config(fg="black")
  249.         if used > 999999:
  250.             unit1 = "Mb"
  251.             used /= 1000000.0
  252.         elif used > 999:
  253.             unit1 = "Kb"
  254.             used /= 1000.0
  255.         if total > 999999:
  256.             unit2 = "Mb"
  257.             total /= 1000000.0
  258.         elif total > 999:
  259.             unit2 = "Kb"
  260.             total /= 1000.0
  261.         self.textStatus.set(string % (used, unit1, total, unit2))
  262.        
  263.     def get_opt(self, widget):
  264.         """ Get some options from a widget then pack them. """
  265.         opt = dict()
  266.         opt["state"] = widget["state"]
  267.         opt["fg"] = widget["fg"]
  268.         opt["bg"] = widget["bg"]
  269.         return opt
  270.        
  271.     def set_state(self):
  272.         """ Enable or disable a widget according to option selected. """
  273.         if self.mode.get(): # write
  274.             self.text.config(**self.textOpt)
  275.         else:
  276.             self.text.config(state="disabled", bg="lightgrey", fg="darkgrey")
  277.         if self.useEncryption.get(): # use AES
  278.             self.keyEntry.config(**self.keyOpt)
  279.             app.useAES = True
  280.         else:
  281.             self.keyEntry.config(state="disabled")
  282.             app.useAES = False
  283.         length = app.parse_string(app.tmp)
  284.         self.usedBytes.set(length)
  285.         self.set_status()
  286.         if self.loaded: # a file is loaded
  287.             if self.mode.get() == 0: # read mode
  288.                 ok = True
  289.             elif app.data != None and self.usedBytes.get() <= self.totalBytes.get():
  290.                 ok = True
  291.             else:
  292.                 ok = False
  293.         else:
  294.             ok = False # no file loaded
  295.         if ok:
  296.             self.startButton.config(state="normal")
  297.         else:
  298.             self.startButton.config(state="disabled")
  299.                
  300.     def handle_event(self, event):
  301.         """ Handle events for specific widgets. """
  302.         if event.widget is self.text and self.text["state"] == "normal":
  303.             if self.text["fg"] == "grey":
  304.                 self.text.delete(0.0, END)
  305.                 self.textOpt["fg"] = self.text["fg"] = "black"
  306.             string = self.text.get(0.0, END).strip()
  307.             try:
  308.                 length = app.parse_string(string)
  309.             except UnicodeError:
  310.                 showerror("Error", "Invalid text.")
  311.             else:
  312.                 self.usedBytes.set(length)
  313.                 self.set_state()
  314.         elif event.widget is self.keyEntry and self.keyEntry["state"] == "normal":
  315.             if self.keyEntry["fg"] == "grey":
  316.                 self.keyEntry.delete(0, END)
  317.                 self.keyOpt["fg"] = self.keyEntry["fg"] = "black"
  318.             key = self.keyEntry.get()[:32] # first 32 (max size is 32)
  319.             try:
  320.                 length = app.parse_key(key)
  321.             except UnicodeError:
  322.                 showerror("Error", "Invalid key.")
  323.             else:
  324.                 self.usedBytes.set(length)
  325.                 self.set_state()
  326.         elif event.widget is self.about:
  327.             showinfo("About", "Hide text, which can be encrypted with AES, in pictures, preferably bitmaps. Coded by cmiN. Visit rstcenter.com")
  328.  
  329.  
  330. if __name__ == "__main__":
  331.     app = Engine() # core
  332.     root = Tk() # toplevel
  333.     root.title("Text In Image")
  334.     root.maxsize(350, 250)
  335.     root.iconbitmap("tii.ico") # comment if you don't have one
  336.     GUI(root)
  337.     root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement