diff --git a/src/Picocrypt.py b/src/Picocrypt.py index 5e55d2e..8f0a9b3 100644 --- a/src/Picocrypt.py +++ b/src/Picocrypt.py @@ -1,34 +1,17 @@ #!/usr/bin/env python3 """ -Dependencies: argon2-cffi, pycryptodome, reedsolo + +Picocrypt v1.11 (Beta) +Dependencies: argon2-cffi, pycryptodome, reedsolo, tkinterdnd2 Copyright (c) Evan Su (https://evansu.cc) Released under a GNU GPL v3 License https://github.com/HACKERALERT/Picocrypt + +~ In cryptography we trust ~ + """ -# Test if libraries are installed -try: - from argon2.low_level import hash_secret_raw - from Crypto.Cipher import ChaCha20_Poly1305 - try: - from creedsolo import ReedSolomonError - except: - from reedsolo import ReedSolomonError -except: - # Libraries missing, install them - from os import system - try: - # Debian/Ubuntu based - system("sudo apt-get install python3-tk") - except: - # Fedora - system("sudo dnf install python3-tkinter") - - system("python3 -m pip install argon2-cffi --no-cache-dir") - system("python3 -m pip install pycryptodome --no-cache-dir") - system("python3 -m pip install reedsolo --no-cache-dir") - # Imports from tkinter import filedialog,messagebox from threading import Thread @@ -37,17 +20,22 @@ from argon2.low_level import hash_secret_raw,Type from Crypto.Cipher import ChaCha20_Poly1305 from Crypto.Hash import SHA3_512 as sha3_512 from secrets import compare_digest -from os import urandom,fsync,remove -from os.path import getsize,expanduser +from os import urandom,fsync,remove,system +from os.path import getsize,expanduser,isdir +from os.path import dirname,abspath,realpath +from os.path import join as pathJoin +from os.path import split as pathSplit +from tkinterdnd2 import TkinterDnD,DND_FILES +from zipfile import ZipFile +from pathlib import Path +from shutil import rmtree import sys import tkinter import tkinter.ttk import tkinter.scrolledtext import webbrowser -try: - from creedsolo import RSCodec,ReedSolomonError -except: - from reedsolo import RSCodec,ReedSolomonError +import platform +from creedsolo import RSCodec,ReedSolomonError # Tk/Tcl is a little barbaric, so I'm disabling # high DPI so it doesn't scale bad and look horrible @@ -57,16 +45,22 @@ try: except: pass -# Global variables and notices +# Global variables and strings +rootDir = dirname(realpath(__file__)) inputFile = "" outputFile = "" +outputPath = "" password = "" ad = "" kept = False working = False gMode = None -headerRsc = None +headerRsc = False +allFiles = False +draggedFolderPaths = False +files = False adString = "File metadata (used to store some text along with the file):" +compressingNotice = "Compressing files together..." passwordNotice = "Error. The provided password is incorrect." corruptedNotice = "Error. The input file is corrupted." veryCorruptedNotice = "Error. The input file and header keys are badly corrupted." @@ -77,16 +71,21 @@ kVeryCorruptedNotice = "The input file is badly corrupted, but the output has be derivingNotice = "Deriving key (takes a few seconds)..." keepNotice = "Keep decrypted output even if it's corrupted or modified" eraseNotice = "Securely erase and delete original file" +erasingNotice = "Securely erasing original file(s)..." overwriteNotice = "Output file already exists. Would you like to overwrite it?" +cancelNotice = "Exiting now will lead to broken output. Are you sure?" rsNotice = "Prevent corruption using Reed-Solomon" rscNotice = "Creating Reed-Solomon tables..." unknownErrorNotice = "Unknown error occured. Please try again." # Create root Tk -tk = tkinter.Tk() -tk.geometry("480x480") +tk = TkinterDnD.Tk() +tk.geometry("480x470") tk.title("Picocrypt") -tk.configure(background="#f5f6f7") +if platform.system()=="Darwin": + tk.configure(background="#edeced") +else: + tk.configure(background="#ffffff") tk.resizable(0,0) # Try setting window icon if included with Picocrypt @@ -98,29 +97,85 @@ except: # Some styling s = tkinter.ttk.Style() -s.configure("TCheckbutton",background="#f5f6f7") +s.configure("TCheckbutton",background="#ffffff") -# Event when user selects an input file -def inputSelected(): - global inputFile,working,headerRsc +# Event when user drags file(s) and folder(s) into window +def inputSelected(draggedFile): + global inputFile,working,headerRsc,allFiles,draggedFolderPaths,files + resetUI() dummy.focus() + status.config(cursor="") + status.bind("",lambda e:None) - # Try to handle when select file is cancelled + # Use try to handle errors try: - # Ask for input file + # Create list of input files + allFiles = [] + files = [] + draggedFolderPaths = [] suffix = "" - tmp = filedialog.askopenfilename( - initialdir=expanduser("~") - ) - if len(tmp)==0: - # Exception will be caught by except below - raise Exception("No file selected.") - inputFile = tmp + tmp = [i for i in draggedFile] + res = [] + within = False + tmpName = "" + + """ + The next for loop parses data return by tkinterdnd2's file drop method. + When files and folders are dragged, the output (the 'draggedFile' parameter) + will contain the dropped files/folders and will look something like this: + + A single file/folder: "C:\Foo\Bar.txt" + A single file/folder with a space in path: "{C:\Foo Bar\Lorem.txt}" + Multiple files/folders: "C:\Foo\Bar1.txt C:\Foo\Ba2.txt" + Multiple files/folders with spaces in paths: + - "C:\Foo\Bar1.txt {C:\Foo Bar\Lorem.txt}" + - "{C:\Foo Bar\Lorem.txt} C:\Foo\Bar1.txt" + - "{C:\Foo Bar\Lorem1.txt} {C:\Foo Bar\Lorem2.txt}" + """ + for i in tmp: + if i=="{": + within = True + elif i=="}": + within = False + res.append(tmpName) + tmpName = "" + else: + if i==" " and not within: + if tmpName!="": + res.append(tmpName) + tmpName = "" + else: + tmpName += i + if tmpName: + res.append(tmpName) + + allFiles = [] + files = [] + + # Check each thing dragged by user + for i in res: + # If there is a directory, recursively add all files to 'allFiles' + if isdir(i): + # Record the directory for secure wipe (if necessary) + draggedFolderPaths.append(i) + tmp = Path(i).rglob("*") + for p in tmp: + allFiles.append(abspath(p)) + # Just a file, add it to files + else: + files.append(i) + + # If there's only one file, set it as input file + if len(files)==1 and len(allFiles)==0: + inputFile = files[0] + files = [] + else: + inputFile = "" # Decide if encrypting or decrypting - if ".pcv" in inputFile.split("/")[-1]: + if inputFile.endswith(".pcv"): suffix = " (will decrypt)" - fin = open(inputFile,"r+b") + fin = open(inputFile,"rb") # Read file metadata (a little complex) tmp = fin.read(139) @@ -148,6 +203,8 @@ def inputSelected(): adArea.delete("1.0",tkinter.END) adArea.insert("1.0",ad) adArea["state"] = "disabled" + + # Update UI adLabelString.set("File metadata (read only):") keepBtn["state"] = "normal" eraseBtn["state"] = "disabled" @@ -155,6 +212,7 @@ def inputSelected(): cpasswordInput["state"] = "normal" cpasswordInput.delete(0,"end") cpasswordInput["state"] = "disabled" + cpasswordString.set("Confirm password (N/A):") else: # Update the UI eraseBtn["state"] = "normal" @@ -166,13 +224,33 @@ def inputSelected(): adLabelString.set(adString) cpasswordInput["state"] = "normal" cpasswordInput.delete(0,"end") + cpasswordString.set("Confirm password:") + cpasswordLabel["state"] = "normal" + adLabel["state"] = "normal" + + nFiles = len(files) + nFolders = len(draggedFolderPaths) + + # Show selected file(s) and folder(s) + if (allFiles or files) and not draggedFolderPaths: + inputString.set(f"{nFiles} files selected (will encrypt).") + elif draggedFolderPaths and not files: + inputString.set(f"{nFolders} folder{'s' if nFolders!=1 else ''} selected (will encrypt).") + elif draggedFolderPaths and (allFiles or files): + inputString.set( + f"{nFiles} file{'s' if nFiles!=1 else ''} and "+ + f"{nFolders} folder{'s' if nFolders!=1 else ''} selected (will encrypt)." + ) + else: + inputString.set(inputFile.split("/")[-1]+suffix) # Enable password box, etc. - inputString.set(inputFile.split("/")[-1]+suffix) passwordInput["state"] = "normal" passwordInput.delete(0,"end") + passwordLabel["state"] = "normal" startBtn["state"] = "normal" statusString.set("Ready.") + status["state"] = "enabled" progress["value"] = 0 # File decode error @@ -180,32 +258,54 @@ def inputSelected(): statusString.set(corruptedNotice) progress["value"] = 100 - # No file selected, do nothing + # No file(s) selected, do nothing except: - pass + inputString.set("Drag and drop file(s) and folder(s) into this window.") + resetUI() # Focus the dummy button to remove ugly borders finally: dummy.focus() working = False -# Button to select input file -selectFileInput = tkinter.ttk.Button( - tk, - text="Select file", - command=inputSelected, -) -selectFileInput.place(x=19,y=20) +# Clears the selected files +def clearInputs(): + dummy.focus() + resetUI() + +# Allow drag and drop +def onDrop(e): + global working + if not working: + inputSelected(e.data) +tk.drop_target_register(DND_FILES) +tk.dnd_bind("<>",onDrop) # Label that displays selected input file inputString = tkinter.StringVar(tk) -inputString.set("Please select a file.") +inputString.set("Drag and drop file(s) and folder(s) into this window.") selectedInput = tkinter.ttk.Label( tk, textvariable=inputString ) -selectedInput.config(background="#f5f6f7") -selectedInput.place(x=104,y=23) +selectedInput.config(background="#ffffff") +selectedInput.place(x=17,y=16) + +# Clear input files +clearInput = tkinter.ttk.Button( + tk, + text="Clear", + command=clearInputs +) +if platform.system()=="Darwin": + clearInput.place(x=398,y=14,width=64,height=24) +else: + clearInput.place(x=421,y=14,width=40,height=24) + +separator = tkinter.ttk.Separator( + tk +) +separator.place(x=20,y=36,width=440) # Label that prompts user to enter a password passwordString = tkinter.StringVar(tk) @@ -214,16 +314,17 @@ passwordLabel = tkinter.ttk.Label( tk, textvariable=passwordString ) -passwordLabel.place(x=17,y=56) -passwordLabel.config(background="#f5f6f7") +passwordLabel.place(x=17,y=46) +passwordLabel.config(background="#ffffff") +passwordLabel["state"] = "disabled" # A frame to make password input fill width passwordFrame = tkinter.Frame( tk, - width=440, + width=(445 if platform.system()=="Darwin" else 440), height=22 ) -passwordFrame.place(x=20,y=76) +passwordFrame.place(x=(17 if platform.system()=="Darwin" else 20),y=66) passwordFrame.columnconfigure(0,weight=10) passwordFrame.grid_propagate(False) # Password input box @@ -240,16 +341,17 @@ cpasswordLabel = tkinter.ttk.Label( tk, textvariable=cpasswordString ) -cpasswordLabel.place(x=17,y=106) -cpasswordLabel.config(background="#f5f6f7") +cpasswordLabel.place(x=17,y=96) +cpasswordLabel.config(background="#ffffff") +cpasswordLabel["state"] = "disabled" # A frame to make confirm password input fill width cpasswordFrame = tkinter.Frame( tk, - width=440, + width=(445 if platform.system()=="Darwin" else 440), height=22 ) -cpasswordFrame.place(x=20,y=126) +cpasswordFrame.place(x=(17 if platform.system()=="Darwin" else 20),y=116) cpasswordFrame.columnconfigure(0,weight=10) cpasswordFrame.grid_propagate(False) # Confirm password input box @@ -262,7 +364,9 @@ cpasswordInput["state"] = "disabled" # Start the encryption/decryption process def start(): - global inputFile,outputFile,password,ad,kept,working,gMode,headerRsc + global inputFile,outputFile,password,ad,kept + global working,gMode,headerRsc,allFiles,files + global dragFolderPath dummy.focus() reedsolo = False chunkSize = 2**20 @@ -277,7 +381,7 @@ def start(): mode = "decrypt" gMode = "decrypt" # Check if Reed-Solomon was enabled by checking for "+" - test = open(inputFile,"rb+") + test = open(inputFile,"rb") decider = test.read(1).decode("utf-8") test.close() if decider=="+": @@ -288,7 +392,7 @@ def start(): # Check if file already exists (getsize() throws error if file not found) try: getsize(outputFile) - force = messagebox.askyesno("Warning",overwriteNotice) + force = messagebox.askyesno("Confirmation",overwriteNotice) dummy.focus() if force!=1: return @@ -296,26 +400,11 @@ def start(): pass # Disable inputs and buttons while encrypting/decrypting - selectFileInput["state"] = "disabled" - passwordInput["state"] = "disabled" - cpasswordInput["state"] = "disabled" - adArea["state"] = "disabled" - startBtn["state"] = "disabled" - eraseBtn["state"] = "disabled" - keepBtn["state"] = "disabled" - rsBtn["state"] = "disabled" + disableAllInputs() # Make sure passwords match if passwordInput.get()!=cpasswordInput.get() and mode=="encrypt": - selectFileInput["state"] = "normal" - passwordInput["state"] = "normal" - cpasswordInput["state"] = "normal" - adArea["state"] = "normal" - startBtn["state"] = "normal" - eraseBtn["state"] = "normal" - rsBtn["state"] = "normal" - working = False - progress["value"] = 100 + resetEncryptionUI() statusString.set("Passwords don't match.") return @@ -328,6 +417,27 @@ def start(): if reedsolo: # 13 bytes per 128 bytes, ~10% larger output file rsc = RSCodec(13) + + # Compress files together if user dragged multiple files + if allFiles or files: + statusString.set(compressingNotice) + tmp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + if files: + zfPath = Path(files[0]).parent.absolute() + else: + zfPath = Path(dirname(allFiles[0])).parent.absolute() + zfOffset = len(str(zfPath)) + zfName = pathJoin(zfPath,tmp+".zip") + zf = ZipFile(zfName,"w") + for i in allFiles: + zf.write(i,i[zfOffset:]) + for i in files: + zf.write(i,pathSplit(i)[1]) + + zf.close() + inputFile = zfName + outputFile = zfName+".pcv" + outputPath = dirname(outputFile) # Set and get some variables working = True @@ -340,7 +450,13 @@ def start(): wipe = erase.get()==1 # Open files - fin = open(inputFile,"rb+") + try: + fin = open(inputFile,"rb") + except: + resetEncryptionUI() + statusString.set("Folder is empty.") + return + if reedsolo and mode=="decrypt": # Move pointer one forward fin.read(1) @@ -425,15 +541,7 @@ def start(): fout.close() remove(outputFile) # Reset UI - selectFileInput["state"] = "normal" - passwordInput["state"] = "normal" - adArea["state"] = "normal" - startBtn["state"] = "normal" - keepBtn["state"] = "normal" - working = False - progress.stop() - progress.config(mode="determinate") - progress["value"] = 100 + resetDecryptionUI() return else: kept = "badlyCorrupted" @@ -472,14 +580,7 @@ def start(): fout.close() remove(outputFile) # Reset UI - selectFileInput["state"] = "normal" - passwordInput["state"] = "normal" - adArea["state"] = "normal" - startBtn["state"] = "normal" - keepBtn["state"] = "normal" - working = False - progress["value"] = 100 - del key + resetDecryptionUI() return # Create XChaCha20-Poly1305 object @@ -492,9 +593,6 @@ def start(): total = getsize(inputFile) # If secure wipe enabled, create a wiper object - if wipe: - wiper = open(inputFile,"r+b") - wiper.seek(0) # Keep track of time because it flies... startTime = datetime.now() @@ -507,11 +605,7 @@ def start(): piece = fin.read(1104905) else: piece = fin.read(chunkSize) - if wipe: - # If securely wipe, write random trash - # to original file after reading it - trash = urandom(len(piece)) - wiper.write(trash) + # If EOF if not piece: if mode=="encrypt": @@ -540,12 +634,7 @@ def start(): if keep.get()!=1: remove(outputFile) # Reset UI - selectFileInput["state"] = "normal" - passwordInput["state"] = "normal" - adArea["state"] = "normal" - startBtn["state"] = "normal" - keepBtn["state"] = "normal" - working = False + resetDecryptionUI() del fin,fout,cipher,key return else: @@ -566,12 +655,7 @@ def start(): if keep.get()!=1: remove(outputFile) # Reset UI - selectFileInput["state"] = "normal" - passwordInput["state"] = "normal" - adArea["state"] = "normal" - startBtn["state"] = "normal" - keepBtn["state"] = "normal" - working = False + resetDecryptionUI() del fin,fout,cipher,key return else: @@ -605,13 +689,7 @@ def start(): fout.close() remove(outputFile) # Reset UI - selectFileInput["state"] = "normal" - passwordInput["state"] = "normal" - adArea["state"] = "normal" - startBtn["state"] = "normal" - keepBtn["state"] = "normal" - working = False - progress["value"] = 100 + resetDecryptionUI() del fin,fout,cipher,key return else: @@ -639,60 +717,76 @@ def start(): data = cipher.decrypt(piece) # Calculate speed, ETA, etc. - first = False elapsed = (datetime.now()-previousTime).total_seconds() or 0.0001 sinceStart = (datetime.now()-startTime).total_seconds() or 0.0001 previousTime = datetime.now() - # Prevent divison by zero - if not elapsed: - elapsed = 0.1**6 + percent = done*100/total progress["value"] = percent - rPercent = round(percent) - speed = (done/sinceStart)/10**6 - # Prevent divison by zero - if not speed: - first = True - speed = 0.1**6 - rSpeed = str(round(speed,2)) - # Right-pad with zeros to large prevent layout shifts - while len(rSpeed.split(".")[1])!=2: - rSpeed += "0" + + speed = (done/sinceStart)/10**6 or 0.0001 eta = round((total-done)/(speed*10**6)) + # Seconds to minutes if seconds more than 59 if eta>=60: - eta = f"{eta//60}m {eta%60}" + # Set blank ETA if just starting + if sinceStart<0.5: + eta = "..." + else: + eta = f"{eta//60}m {eta%60}" if isinstance(eta,int) or isinstance(eta,float): if eta<0: eta = 0 - # If it's the first round and no data/predictions yet... - if first: - statusString.set("...% at ... MB/s (ETA: ...s)") - else: - # Update status - info = f"{rPercent}% at {rSpeed} MB/s (ETA: {eta}s)" - if reedsolo and mode=="decrypt" and reedsoloFixedCount: - eng = "s" if reedsoloFixedCount!=1 else "" - info += f", fixed {reedsoloFixedCount} corrupted byte{eng}" - if reedsolo and mode=="decrypt" and reedsoloErrorCount: - info += f", {reedsoloErrorCount} MB unrecoverable" - statusString.set(info) + + # Update status + info = f"{percent:.0f}% at {speed:.2f} MB/s (ETA: {eta}s)" + + if reedsolo and mode=="decrypt" and reedsoloFixedCount: + tmp = "s" if reedsoloFixedCount!=1 else "" + info += f", fixed {reedsoloFixedCount} corrupted byte{tmp}" + if reedsolo and mode=="decrypt" and reedsoloErrorCount: + info += f", {reedsoloErrorCount} MB unrecoverable" + + statusString.set(info) # Increase done and write to output done += 1104905 if (reedsolo and mode=="decrypt") else chunkSize fout.write(data) + # Flush outputs, close files + if not kept: + fout.flush() + fsync(fout.fileno()) + fout.close() + fin.close() + + # Securely wipe files as necessary + if wipe: + if draggedFolderPaths: + for i in draggedFolderPaths: + secureWipe(i) + if files: + for i in range(len(files)): + statusString.set(erasingNotice+f" ({i}/{len(files)}") + progress["value"] = i/len(files) + secureWipe(files[i]) + secureWipe(inputFile) + # Secure wipe not enabled + else: + if allFiles: + # Remove temporary zip file if created + remove(inputFile) + # Show appropriate notice if file corrupted or modified if not kept: - if mode=="encrypt": - output = inputFile.split("/")[-1]+".pcv" - else: - output = inputFile.split("/")[-1].replace(".pcv","") - statusString.set(f"Completed. (Output: {output})") + statusString.set(f"Completed. (Click here to show output)") + # Show Reed-Solomon stats if it fixed corrupted bytes if mode=="decrypt" and reedsolo and reedsoloFixedCount: - statusString.set(f"Completed with {reedsoloFixedCount} bytes fixed."+ - f" (Output: {output})") + statusString.set( + f"Completed with {reedsoloFixedCount}"+ + f" bytes fixed. (Output: {output})" + ) else: if kept=="modified": statusString.set(kModifiedNotice) @@ -701,45 +795,32 @@ def start(): else: statusString.set(kVeryCorruptedNotice) + status.config(cursor="hand2") + + # A little hack since strings are immutable + output = "".join([i for i in outputFile]) + + # Bind the output file + if platform.system()=="Windows": + status.bind("", + lambda e:showOutput(output.replace("/","\\")) + ) + else: + status.bind("", + lambda e:showOutput(output) + ) # Reset variables and UI states - selectFileInput["state"] = "normal" - adArea["state"] = "normal" - adArea.delete("1.0",tkinter.END) - adArea["state"] = "disabled" - startBtn["state"] = "disabled" - passwordInput["state"] = "normal" - passwordInput.delete(0,"end") - passwordInput["state"] = "disabled" - cpasswordInput["state"] = "normal" - cpasswordInput.delete(0,"end") - cpasswordInput["state"] = "disabled" - progress["value"] = 0 - inputString.set("Please select a file.") - keepBtn["state"] = "normal" - keep.set(0) - keepBtn["state"] = "disabled" - eraseBtn["state"] = "normal" - erase.set(0) - eraseBtn["state"] = "disabled" - rs.set(0) - rsBtn["state"] = "disabled" - if not kept: - fout.flush() - fsync(fout.fileno()) - fout.close() - fin.close() - if wipe: - # Make sure to flush file - wiper.flush() - fsync(wiper.fileno()) - wiper.close() - remove(inputFile) + resetUI() + status["state"] = "normal" inputFile = "" outputFile = "" password = "" ad = "" kept = False working = False + allFiles = False + dragFolderPath = False + # Wipe keys for safety del fin,fout,cipher,key @@ -751,34 +832,124 @@ def wrapper(): start() except: # Reset UI accordingly - progress.stop() - progress.config(mode="determinate") - progress["value"] = 100 - selectFileInput["state"] = "normal" - passwordInput["state"] = "normal" - startBtn["state"] = "normal" if gMode=="decrypt": - keepBtn["state"] = "normal" + resetDecryptionUI() else: - adArea["state"] = "normal" - cpasswordInput["state"] = "normal" - rsBtn["state"] = "normal" - eraseBtn["state"] = "normal" + resetEncryptionUI() statusString.set(unknownErrorNotice) dummy.focus() - working = False finally: sys.exit(0) -# Encryption/decrypt is done is a separate thread -# so the UI isn't blocked. This is a wrapper -# to spawn a thread and start it. +# Encryption/decrypt is done is a separate thread so the UI +# isn't blocked. This is a wrapper to spawn a thread and start it. def startWorker(): thread = Thread(target=wrapper,daemon=True) thread.start() +# Securely wipe file +def secureWipe(fin): + statusString.set(erasingNotice) + # Check platform, erase accordingly + if platform.system()=="Windows": + if isdir(fin): + paths = [] + for i in Path(fin).rglob("*"): + if dirname(i) not in paths: + paths.append(dirname(i)) + for i in range(len(paths)): + statusString.set(erasingNotice+f" ({i}/{len(paths)})") + progress["value"] = 100*i/len(paths) + system(f'cd "{paths[i]}" && "{rootDir}/sdelete64.exe" * -p 4 -s -nobanner') + system(f'cd "{rootDir}"') + rmtree(fin) + else: + statusString.set(erasingNotice) + progress["value"] = 100 + system(f'sdelete64.exe "{fin}" -p 4 -nobanner') + elif platform.system()=="Darwin": + system(f'rm -rfP "{fin}"') + else: + system(f'shred -uz "{fin}" -n 4') + +# Disable all inputs while encrypting/decrypting +def disableAllInputs(): + passwordInput["state"] = "disabled" + cpasswordInput["state"] = "disabled" + adArea["state"] = "disabled" + startBtn["state"] = "disabled" + eraseBtn["state"] = "disabled" + keepBtn["state"] = "disabled" + rsBtn["state"] = "disabled" + +# Reset UI to encryption state +def resetEncryptionUI(): + global working + passwordInput["state"] = "normal" + cpasswordInput["state"] = "normal" + adArea["state"] = "normal" + startBtn["state"] = "normal" + eraseBtn["state"] = "normal" + rsBtn["state"] = "normal" + working = False + progress.stop() + progress.config(mode="determinate") + progress["value"] = 100 + +# Reset UI to decryption state +def resetDecryptionUI(): + global working + passwordInput["state"] = "normal" + adArea["state"] = "normal" + startBtn["state"] = "normal" + keepBtn["state"] = "normal" + working = False + progress.stop() + progress.config(mode="determinate") + progress["value"] = 100 + +# Reset UI to original state (no file selected) +def resetUI(): + adArea["state"] = "normal" + adArea.delete("1.0",tkinter.END) + adArea["state"] = "disabled" + adLabel["state"] = "disabled" + startBtn["state"] = "disabled" + passwordInput["state"] = "normal" + passwordInput.delete(0,"end") + passwordInput["state"] = "disabled" + passwordLabel["state"] = "disabled" + cpasswordInput["state"] = "normal" + cpasswordInput.delete(0,"end") + cpasswordInput["state"] = "disabled" + cpasswordString.set("Confirm password:") + cpasswordLabel["state"] = "disabled" + status["state"] = "disabled" + progress["value"] = 0 + inputString.set("Drag and drop file(s) and folder(s) into this window.") + keepBtn["state"] = "normal" + keep.set(0) + keepBtn["state"] = "disabled" + eraseBtn["state"] = "normal" + erase.set(0) + eraseBtn["state"] = "disabled" + rs.set(0) + rsBtn["state"] = "disabled" + progress.stop() + progress.config(mode="determinate") + progress["value"] = 0 + +def showOutput(file): + if platform.system()=="Windows": + system(f'explorer /select,"{file}"') + elif platform.system()=="Darwin": + system(f'cd "{dirname(file)}"; open -R {pathSplit(file)[1]}') + system(f'cd "{rootDir}"') + else: + system(f'xdg-open "{dirname(file)}"') + # ad stands for "associated data"/metadata adLabelString = tkinter.StringVar(tk) adLabelString.set(adString) @@ -786,8 +957,9 @@ adLabel = tkinter.ttk.Label( tk, textvariable=adLabelString ) -adLabel.place(x=17,y=158) -adLabel.config(background="#f5f6f7") +adLabel.place(x=17,y=148) +adLabel.config(background="#ffffff") +adLabel["state"] = "disabled" # Frame so metadata text box can fill width adFrame = tkinter.Frame( @@ -795,7 +967,7 @@ adFrame = tkinter.Frame( width=440, height=100 ) -adFrame.place(x=20,y=178) +adFrame.place(x=20,y=168) adFrame.columnconfigure(0,weight=10) adFrame.grid_propagate(False) @@ -818,7 +990,7 @@ keepBtn = tkinter.ttk.Checkbutton( offvalue=0, command=lambda:dummy.focus() ) -keepBtn.place(x=18,y=290) +keepBtn.place(x=18,y=280) keepBtn["state"] = "disabled" # Check box for securely erasing original file @@ -831,7 +1003,7 @@ eraseBtn = tkinter.ttk.Checkbutton( offvalue=0, command=lambda:dummy.focus() ) -eraseBtn.place(x=18,y=310) +eraseBtn.place(x=18,y=300) eraseBtn["state"] = "disabled" # Check box for Reed Solomon @@ -844,16 +1016,16 @@ rsBtn = tkinter.ttk.Checkbutton( offvalue=0, command=lambda:dummy.focus() ) -rsBtn.place(x=18,y=330) +rsBtn.place(x=18,y=320) rsBtn["state"] = "disabled" # Frame so start button can fill width startFrame = tkinter.Frame( tk, width=442, - height=25 + height=24 ) -startFrame.place(x=19,y=360) +startFrame.place(x=19,y=350) startFrame.columnconfigure(0,weight=10) startFrame.grid_propagate(False) # Start button @@ -872,7 +1044,7 @@ progress = tkinter.ttk.Progressbar( length=440, mode="determinate" ) -progress.place(x=20,y=388) +progress.place(x=20,y=378) # Status label statusString = tkinter.StringVar(tk) @@ -881,8 +1053,9 @@ status = tkinter.ttk.Label( tk, textvariable=statusString ) -status.place(x=17,y=416) -status.config(background="#f5f6f7") +status.place(x=17,y=406) +status.config(background="#ffffff") +status["state"] = "disabled" # Credits :) hint = "Created by Evan Su. Click for details and source." @@ -894,21 +1067,21 @@ credits = tkinter.ttk.Label( cursor="hand2" ) credits["state"] = "disabled" -credits.config(background="#f5f6f7") -credits.place(x=17,y=446) +credits.config(background="#ffffff") +credits.place(x=17,y=436) source = "https://github.com/HACKERALERT/Picocrypt" credits.bind("",lambda e:webbrowser.open(source)) # Version versionString = tkinter.StringVar(tk) -versionString.set("v1.10") +versionString.set("v1.11") version = tkinter.ttk.Label( tk, textvariable=versionString ) version["state"] = "disabled" -version.config(background="#f5f6f7") -version.place(x=430,y=446) +version.config(background="#ffffff") +version.place(x=(420 if platform.system()=="Darwin" else 430),y=436) # Dummy button to remove focus from other buttons # and prevent ugly border highlighting @@ -922,17 +1095,29 @@ def createRsc(): global headerRsc headerRsc = RSCodec(128) sys.exit(0) + +def prepare(): + if platform.system()=="Windows": + system("sdelete64.exe /accepteula") # Close window only if not encrypting or decrypting def onClose(): + global outputFile if not working: tk.destroy() + else: + force = messagebox.askyesno("Confirmation",cancelNotice) + if force: + tk.destroy() # Main application loop if __name__=="__main__": # Create Reed-Solomon header codec tmp = Thread(target=createRsc,daemon=True) tmp.start() + # Prepare application + tmp = Thread(target=prepare,daemon=True) + tmp.start() # Start tkinter tk.protocol("WM_DELETE_WINDOW",onClose) tk.mainloop()