cmiN Posted January 5, 2012 Report Posted January 5, 2012 (edited) ScreenDupa cum spune si titlul, acest soft ascunde text in imagini, pentru mai multe detalii vezi comentariul sursei.L-am pus aici ca sa ramana mai mult timp "vizibil", daca e vreo problema il mut la stuff.In 3 zile, 300 linii de cod, m-am gandit sa ma rezolv cat de cat cu atestatul si cum iubesc sursa libera si impartasirea ei pun la dispozitie asta:http://codepad.org/hNzxFzFT#! /usr/bin/env python# Text In Image# 02.01.2012 cmiN## This is a simple GUI script which can hide text in pictures# using least significant bit method.# Also the input text can be encrypted and the output can be decrypted too# with a symmetric key using AES.# Writing is done directly on input image so be careful with certain extensions# because the output will always have the BMP format.## Contact: cmin764@yahoo/gmail.comfrom Tkinter import * # widgets's classesfrom tkFileDialog import askopenfilename # get file namefrom tkMessageBox import showerror, showinfo # user dialogfrom PIL import Image # image convertingfrom Crypto.Cipher import AES # text cipherclass Engine: """ Code for processing the image. Separated from GUI. """ def __init__(self): """ Initialize parameters. """ self.ext = "bmp" # save format self.name = None # save name self.path = None # save path self.im = None # image object, read and write self.generator = None # get locations to write/read bits self.useAES = None # use it or not self.aes = None # AES object self.data = None # data to be written to image self.width = None # image width self.height = None # image height self.tmp = None # last string, used when key changes def binary(self, nr, size): """ Get 1&0 representation. """ return bin(nr).replace("0b", "").zfill(size * 8) def path_name(self, path): """ Split a file path in path and name. """ ind = path.rfind("/") + 1 return (path[:ind], path[ind:]) def set_generator(self): """ Useful for resetting. """ self.generator = ((wp, hp, ch) for wp in xrange(self.width) # WxHxC for hp in xrange(self.height) for ch in xrange(3)) def load(self, path): """ Load image. """ self.im = Image.open(path) (self.width, self.height) = self.im.size (self.path, self.name) = self.path_name(path) return self.width * self.height * 3 # total useful bytes def parse_key(self, key): """ If key exists make an AES object from it. """ if not key: self.aes = None # empty key == no encryption return self.parse_string(self.tmp) # must return size (see the next return) key.decode() # test availability size = len(key) for padding in (16, 24, 32): # fixed key size if size <= padding: break key += chr(0) * (padding - size) self.aes = AES.new(key) return self.parse_string(self.tmp) # if key changes you must update string def parse_string(self, string): """ Convert to bitstring. """ if not string: # without string can't start the process self.tmp = None self.data = None return 0 string.decode() # test availability self.tmp = string if self.useAES and self.aes: # encrypt it string += chr(0) * ((16 - len(string) % 16) % 16) # multiple of 16 string string = self.aes.encrypt(string) string = str().join([self.binary(ord(x), 1) for x in string]) # convert every char in a set of 8 bits size = self.binary(len(string), 4) # get binary representation of string's length in 4 bytes self.data = size + string return len(self.data) def write(self): """ Write using LSB. """ self.set_generator() # rearm for bit in self.data: (wp, hp, ch) = self.generator.next() # get next position values = list(self.im.getpixel((wp, hp))) # retrieve its values tmp = self.binary(values[ch], 1) # convert one of them values[ch] = int(tmp[:7] + bit, 2) # alter that channel self.im.putpixel((wp, hp), tuple(values)) # put it back self.im.save(self.path + self.name, format=self.ext) # save the new output def read(self): """ Read from every LSB. """ self.set_generator() # rearm total = self.width * self.height * 3 if total < 32: raise Exception("Text not found.") size = chunk = string = str() i = 0 # for(i=0; true; ++i) while True: (wp, hp, ch) = self.generator.next() # i byte values = self.im.getpixel((wp, hp)) tmp = self.binary(values[ch], 1) if i < 32: # it's lame but I prefer string/bitset size += tmp[7] if i == 31: size = int(size, 2) if size < 1 or (size + 32) > total: raise Exception("Text not found.") elif i < size + 32: chunk += tmp[7] if len(chunk) == 8: string += chr(int(chunk, 2)) chunk = str() else: break i += 1 if self.useAES and self.aes: if len(string) % 16 != 0: raise Exception("Text not encrypted.") string = self.aes.decrypt(string).rstrip(chr(0)) string.decode() # raise an exception if invalid return stringclass GUI(Frame): """ Main window, inherited from Frame. Here we put our widgets and set their behavior. """ def __init__(self, master=None, margin=30): """ Same as Frame's constructor. """ Frame.__init__(self, master, padx=margin, pady=margin) self.grid() self.widgets() self.behavior() def widgets(self): """ Build and grid widgets. """ # ---- create variables ---- self.totalBytes = IntVar() # depends on image size self.usedBytes = IntVar() # how many of them are used self.textStatus = StringVar() # used per total bytes self.useEncryption = IntVar() # 0-plain 1-AES self.mode = IntVar() # 0-read 1-write self.textOpt = dict() # text last config self.keyOpt = dict() # key last config self.loaded = False # image loaded or not # ---- create widgets ---- self.label = Label(self, textvariable=self.textStatus) self.about = Label(self, text="About", fg="blue") self.text = Text(self, width=30, height=5, fg="grey") self.scrollbar = Scrollbar(self, orient="vertical", command=self.text.yview) self.loadButton = Button(self, text="Load", width=5, command=lambda: self.action("load")) self.readRadio = Radiobutton(self, text="Read", variable=self.mode, value=0, command=self.set_state) self.checkButton = Checkbutton(self, text="Use AES", variable=self.useEncryption, onvalue=1, offvalue=0, command=self.set_state) self.startButton = Button(self, text="Start", width=5, state="disabled", command=lambda: self.action("start")) self.writeRadio = Radiobutton(self, text="Write", variable=self.mode, value=1, command=self.set_state) self.keyEntry = Entry(self, width=10, fg="grey", show="*") # ---- show widgets ---- self.label.grid(row=0, column=0, columnspan=2, sticky="w") self.about.grid(row=0, column=2, sticky="e") self.text.grid(row=1, column=0, rowspan=3, columnspan=3) self.scrollbar.grid(row=1, column=3, rowspan=3, sticky="ns") self.loadButton.grid(row=4, column=0, sticky="w", pady=5) self.readRadio.grid(row=4, column=1) self.checkButton.grid(row=4, column=2, sticky="e") self.startButton.grid(row=5, column=0, sticky="w") self.writeRadio.grid(row=5, column=1) self.keyEntry.grid(row=5, column=2, sticky="e") def behavior(self): """ Customize widgets. """ self.text.config(yscrollcommand=self.scrollbar.set) self.text.insert(0.0, "Text here") self.keyEntry.insert(0, "Key here") self.text.bind("<Button>", self.handle_event) self.text.bind("<KeyRelease>", self.handle_event) self.keyEntry.bind("<Button>", self.handle_event) self.keyEntry.bind("<KeyRelease>", self.handle_event) self.textOpt = self.get_opt(self.text) self.keyOpt = self.get_opt(self.keyEntry) self.about.bind("<Button>", self.handle_event) self.set_state() def action(self, arg): """ What every button triggers. """ if arg == "load": fileTypes = [("BMP", "*.bmp"), ("JPEG", ("*.jpeg", "*.jpg")), ("PNG", "*.png"), ("All Files", "*.*")] path = askopenfilename(parent=self, title="Open image", filetypes=fileTypes) if path != "": try: self.totalBytes.set(app.load(path)) except IOError as msg: showerror("Error", str(msg).capitalize().strip(".") + ".") # some formatting else: self.loaded = True self.set_state() self.master.title("Text In Image - %s" % app.name) # update name in title elif arg == "start": if self.mode.get(): try: app.write() except Exception as msg: showerror("Error", str(msg).capitalize().strip(".") + ".") else: showinfo("Info", "Done.") else: try: string = app.read() except UnicodeError: showerror("Error", "Text not found or wrong key.") except Exception as msg: showerror("Error", str(msg).capitalize().strip(".") + ".") else: self.text.config(state="normal") self.textOpt["fg"] = "black" # touched self.text.delete(0.0, END) self.text.insert(0.0, string) self.text.config(state="disabled") self.usedBytes.set(app.parse_string(string)) self.set_status() showinfo("Info", "Done.") def set_status(self): """ Get used per total bytes. """ string = "%9.3f%s/%9.3f%s" unit1 = unit2 = "b" used = self.usedBytes.get() total = self.totalBytes.get() if used > total: self.label.config(fg="red") else: self.label.config(fg="black") if used > 999999: unit1 = "Mb" used /= 1000000.0 elif used > 999: unit1 = "Kb" used /= 1000.0 if total > 999999: unit2 = "Mb" total /= 1000000.0 elif total > 999: unit2 = "Kb" total /= 1000.0 self.textStatus.set(string % (used, unit1, total, unit2)) def get_opt(self, widget): """ Get some options from a widget then pack them. """ opt = dict() opt["state"] = widget["state"] opt["fg"] = widget["fg"] opt["bg"] = widget["bg"] return opt def set_state(self): """ Enable or disable a widget according to option selected. """ if self.mode.get(): # write self.text.config(**self.textOpt) else: self.text.config(state="disabled", bg="lightgrey", fg="darkgrey") if self.useEncryption.get(): # use AES self.keyEntry.config(**self.keyOpt) app.useAES = True else: self.keyEntry.config(state="disabled") app.useAES = False length = app.parse_string(app.tmp) self.usedBytes.set(length) self.set_status() if self.loaded: # a file is loaded if self.mode.get() == 0: # read mode ok = True elif app.data != None and self.usedBytes.get() <= self.totalBytes.get(): ok = True else: ok = False else: ok = False # no file loaded if ok: self.startButton.config(state="normal") else: self.startButton.config(state="disabled") def handle_event(self, event): """ Handle events for specific widgets. """ if event.widget is self.text and self.text["state"] == "normal": if self.text["fg"] == "grey": self.text.delete(0.0, END) self.textOpt["fg"] = self.text["fg"] = "black" string = self.text.get(0.0, END).strip() try: length = app.parse_string(string) except UnicodeError: showerror("Error", "Invalid text.") else: self.usedBytes.set(length) self.set_state() elif event.widget is self.keyEntry and self.keyEntry["state"] == "normal": if self.keyEntry["fg"] == "grey": self.keyEntry.delete(0, END) self.keyOpt["fg"] = self.keyEntry["fg"] = "black" key = self.keyEntry.get()[:32] # first 32 (max size is 32) try: length = app.parse_key(key) except UnicodeError: showerror("Error", "Invalid key.") else: self.usedBytes.set(length) self.set_state() elif event.widget is self.about: showinfo("About", "Hide text, which can be encrypted with AES, in pictures, preferably bitmaps. Coded by cmiN. Visit rstcenter.com")if __name__ == "__main__": app = Engine() # core root = Tk() # toplevel root.title("Text In Image") root.maxsize(350, 250) root.iconbitmap("tii.ico") # comment if you don't have one GUI(root) root.mainloop()Testat pe windows, fedora si slackware, merge perfect, dar sa cititi comentariul principal.Aveti nevoie de python 2.7, PIL si pycrypto. Pe linux de obicei sunt preinstalate. Versiune portabila:boxgamefrontUpdated: 14.01.2012 Edited July 17, 2013 by cmiN 3 Quote
cmiN Posted January 5, 2012 Author Report Posted January 5, 2012 Pune-i extensia .py in caz de ai .pyw (ca sa-ti afiseze consola) si vezi daca da in output vreo eroare, eventual incearca sa faci mai intai setarile si apoi sa dai load ca sunt curios daca se comporta la fel. Pe win i-am testat comportamentul in toate felurile, logica e buna, dar pe alte platforme daca da vreo eroare din cauza vreounui pachet nu mai face anumite chestii. Quote
Flubber Posted January 5, 2012 Report Posted January 5, 2012 Confirmare Fedora 14:Scriere: Decriptare: Insa dupa scriere, avem o problema: Byte-ul respectiv ce specifica "BMP" in hexedit: Pentru a repara (asa cum a specificat si cmiN) se redenumeste fisierul cu extensia ".bmp", et voila.Dupa redenumire inca se poate citi (evident): Quote
Active Members Fi8sVrs Posted January 5, 2012 Active Members Report Posted January 5, 2012 (edited) in windows cu python 2.7 functioneaza si *.jpg+rep Edited January 5, 2012 by Fi8sVrs Quote
cmiN Posted January 5, 2012 Author Report Posted January 5, 2012 Tot .bmp salveaza, dar daca modificam extensia imi dadea bataie de cap fisierul, pentru ca daca avea extensie diferita trebuia sa sterg sursa si sa-l las pe asta nou, fiindca as fi avut 2 fisiere pe urma din cauza extensiei ce diferea. Oricum pe windows nu face galagie, iar pe slackware o sa testez dupa ce mi-l pun si eu, dar dupa comportament e clar ca da o eroare undeva. Aveti grija la versiunea de PIL sau cum e instalata ca pe unele distributii nu au decodere. M-am chinuit cu PIL pentru ceva mai multa compatiblitate si pentru un alt mod de scriere a bitilor fata de celelalte softuri de genul.Cand o sa lansez versiune finala o sa-l "inghet" pentru win intr-o arhiva. Multumesc tuturor . Quote
Active Members Fi8sVrs Posted January 5, 2012 Active Members Report Posted January 5, 2012 (edited) http://uploadimage.ro/images/06871873589185815933.jpgpass: rstcenter Edited January 5, 2012 by Fi8sVrs Quote
cmiN Posted January 6, 2012 Author Report Posted January 6, 2012 Testat si pe slackware, doar am luat si instalat din tar.gz pycrypto, in rest am folosit py2.6 si PIL-ul standard si a mers si la conversie (cu alte extensii). Quote
cmiN Posted January 14, 2012 Author Report Posted January 14, 2012 Updated!Am rezolvat toate bugurile, ceva modificari in interfata, versiuni portabile pentru windows (fara dependente) si acum afiseaza un mesaj dupa ce s-a executat cu succes comanda lui Start. Quote