From 34959b4d8a7f0dd4f7bd098e70536c64e0c9b0d3 Mon Sep 17 00:00:00 2001 From: Evan Su <48808396+HACKERALERT@users.noreply.github.com> Date: Tue, 30 Mar 2021 09:31:19 -0400 Subject: [PATCH] Update Picocrypt.py --- src/Picocrypt.py | 1610 ++++++++++++++++++++++++---------------------- 1 file changed, 830 insertions(+), 780 deletions(-) diff --git a/src/Picocrypt.py b/src/Picocrypt.py index b9bdc5b..616d3cb 100644 --- a/src/Picocrypt.py +++ b/src/Picocrypt.py @@ -2,7 +2,8 @@ """ -Picocrypt v1.12 +Picocrypt v1.11 +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 @@ -11,426 +12,113 @@ https://github.com/HACKERALERT/Picocrypt """ -# Import dependencies +# Imports +from tkinter import filedialog,messagebox from threading import Thread from datetime import datetime -from argon2.low_level import hash_secret_raw -from argon2.low_level import Type as argonType +from argon2.low_level import hash_secret_raw,Type from Crypto.Cipher import ChaCha20_Poly1305 -from Crypto.Hash import SHA3_512,BLAKE2b -from hmac import compare_digest -from creedsolo import RSCodec,ReedSolomonError +from Crypto.Hash import SHA3_512 as sha3_512 +from secrets import compare_digest from os import urandom,fsync,remove,system -from os.path import getsize,expanduser,isdir,exists,dirname,abspath,realpath -from os.path import join as pathJoin,split as pathSplit -from pathlib import Path -from zipfile import ZipFile +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 ttkthemes import ThemedStyle -from time import sleep -import re +from zipfile import ZipFile +from pathlib import Path +from shutil import rmtree import sys import tkinter import tkinter.ttk import tkinter.scrolledtext import webbrowser import platform +from creedsolo import RSCodec,ReedSolomonError -# Global variables +# Tk/Tcl is a little barbaric, so I'm disabling +# high DPI so it doesn't scale bad and look horrible +try: + from ctypes import windll + windll.shcore.SetProcessDpiAwareness(0) +except: + pass + +# Global variables and strings rootDir = dirname(realpath(__file__)) +inputFile = "" +outputFile = "" +outputPath = "" +password = "" +ad = "" +kept = False working = False -mode = False -inputFile = False -outputFile = False -rs128 = False -rs13 = False +gMode = None +headerRsc = False allFiles = False -onlyFolders = False -onlyFiles = False -startTime = False -previousTime = False -done = False -stopUpdating = False -reedsolo = False -reedsoloFixed = False -reedsoloErrors = 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." +modifiedNotice = "Error. The input file has been intentionally modified." +kCorruptedNotice = "The input file is corrupted, but the output has been kept." +kModifiedNotice = "The input file has been intentionally modified, but the output has been kept." +kVeryCorruptedNotice = "The input file is badly corrupted, but the output has been kept." +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." -# Strings -strings = [ - "File metadata (used to store some text along with the file):", - "Compressing files together...", - "Error. The provided password is incorrect.", - "Error. The input file is corrupted.", - "Error. The input file and header keys are badly corrupted.", - "Error. The input file has been intentionally modified.", - "The input file is corrupted, but the output has been kept.", - "The input file has been intentionally modified, but the output has been kept.", - "The input file is badly corrupted, but the output has been kept.", - "Deriving key (takes a few seconds)...", - "Keep decrypted output even if it's corrupted or modified", - "Securely erase and delete original file", - "Securely erasing original file(s)...", - "Output file already exists. Would you like to overwrite it?", - "Exiting now will lead to broken output. Are you sure?", - "Prevent corruption using Reed-Solomon", - "Error. Folder(s) and/or file(s) are empty.", - "Unknown error occured. Please try again.", - "Drag and drop file(s) and folder(s) into this window.", - "File metadata (read-only):", - "Error. The input file couldn't be decoded as UTF-8." -] - -# Create root tk +# Create root Tk tk = TkinterDnD.Tk() -tk.geometry("480x512") +tk.geometry("480x470") tk.title("Picocrypt") +if platform.system()=="Darwin": + tk.configure(background="#edeced") +else: + tk.configure(background="#ffffff") tk.resizable(0,0) -tk.configure(background="#f5f6f7") -ThemedStyle(tk).set_theme("arc") -# Enable high DPI on Windows -def Get_HWND_DPI(window_handle): - from ctypes import windll,pointer,wintypes - windll.shcore.SetProcessDpiAwareness(1) - DPI100pc = 96 - DPI_type = 0 - winH = wintypes.HWND(window_handle) - monitorhandle = windll.user32.MonitorFromWindow( - winH,wintypes.DWORD(2) - ) - X = wintypes.UINT() - Y = wintypes.UINT() - try: - windll.shcore.GetDpiForMonitor( - monitorhandle,DPI_type,pointer(X),pointer(Y) - ) - return X.value*2,Y.value*2,(X.value+Y.value)/(2*DPI100pc) - except Exception: - return 96,96,1 -def TkGeometryScale(s,cvtfunc): - patt = r"(?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+)" - R = re.compile(patt).search(s) - G = str(cvtfunc(R.group("W")))+"x" - G += str(cvtfunc(R.group("H")))+"+" - G += str(cvtfunc(R.group("X")))+"+" - G += str(cvtfunc(R.group("Y"))) - return G -def MakeTkDPIAware(TKGUI): - TKGUI.DPI_X,TKGUI.DPI_Y,TKGUI.DPI_scaling = Get_HWND_DPI(TKGUI.winfo_id()) - TKGUI.TkScale = lambda v:int(float(v)*TKGUI.DPI_scaling) - TKGUI.TkGeometryScale = lambda s:TkGeometryScale(s,TKGUI.TkScale) -if platform.system()=="Windows": - MakeTkDPIAware(tk) - -# Add some styling -style = tkinter.ttk.Style() - -# Try setting window icon if it exists +# Try setting window icon if included with Picocrypt try: favicon = tkinter.PhotoImage(file="./key.png") tk.iconphoto(False,favicon) except: pass -# Dummy button used for removing ugly highlights -dummy = tkinter.ttk.Button(tk) -dummy.place(x=480,y=0) +# Some styling +s = tkinter.ttk.Style() +s.configure("TCheckbutton",background="#ffffff") -# Label that shows the input file(s) -inputString = tkinter.StringVar(tk) -inputString.set(strings[18]) -inputLabel = tkinter.ttk.Label( - tk, - textvariable=inputString -) -inputLabel.place(x=20,y=18) - -# Clear input file(s) -clearInput = tkinter.ttk.Button( - tk, - text="Clear", - command = lambda:resetUI() -) -clearInput.place(x=400,y=12,width=60,height=28) -clearInput["state"] = "disabled" - -# Separator for aesthetics -separator = tkinter.Frame( - tk, - bg="#dfe3ed", - height=1 -) -separator.place(x=20,y=39,width=438) - -# Label to ask user what to save output as -outputString = tkinter.StringVar(tk) -outputString.set("Save output as:") -outputLabel = tkinter.ttk.Label( - tk, - textvariable=outputString -) -outputLabel.place(x=20,y=48) - -outputLabel["state"] = "disabled" - -# A ".pcv" extension shown next to output box -pcvString = tkinter.StringVar(tk) -pcvString.set(".pcv") -pcvLabel = tkinter.ttk.Label( - tk, - textvariable=pcvString -) -pcvLabel.place(x=434,y=68) - -# A frame to allow output box to fill width -outputFrame = tkinter.Frame( - tk, - width=440, - height=24 -) -outputFrame.place(x=20,y=66) -outputFrame.columnconfigure(0,weight=10) -outputFrame.grid_propagate(False) - -# Output box to allow user to change output name and path -outputInput = tkinter.ttk.Entry(outputFrame) -outputInput.grid(sticky="nesw") -outputInput["state"] = "disabled" - -# Prompt user to enter password -passwordString = tkinter.StringVar(tk) -passwordString.set("Password:") -passwordLabel = tkinter.ttk.Label( - tk, - textvariable=passwordString -) -passwordLabel.place(x=20,y=100) -passwordLabel["state"] = "disabled" - -# Allow password input to fill width -passwordFrame = tkinter.Frame( - tk, - width=440, - height=24 -) -passwordFrame.place(x=20,y=118) -passwordFrame.columnconfigure(0,weight=10) -passwordFrame.grid_propagate(False) - -# Password input box -passwordInput = tkinter.ttk.Entry( - passwordFrame, - show="\u2022" -) -passwordInput.grid(sticky="nesw") -passwordInput["state"] = "disabled" - -# Prompt user to confirm password -cPasswordString = tkinter.StringVar(tk) -cPasswordString.set("Confirm password:") -cPasswordLabel = tkinter.ttk.Label( - tk, - textvariable=cPasswordString -) -cPasswordLabel.place(x=20,y=150) -cPasswordLabel["state"] = "disabled" - -# Allow confirm password input to fill width -cPasswordFrame = tkinter.Frame( - tk, - width=440, - height=24 -) -cPasswordFrame.place(x=20,y=168) -cPasswordFrame.columnconfigure(0,weight=10) -cPasswordFrame.grid_propagate(False) - -# Confirm password input box -cPasswordInput = tkinter.ttk.Entry( - cPasswordFrame, - show="\u2022" -) -cPasswordInput.grid(sticky="nesw") -cPasswordInput["state"] = "disabled" - -# Prompt user for optional metadata -metadataString = tkinter.StringVar(tk) -metadataString.set(strings[0]) -metadataLabel = tkinter.ttk.Label( - tk, - textvariable=metadataString -) -metadataLabel.place(x=20,y=202) -metadataLabel["state"] = "disabled" - -# Frame so metadata box can fill width -metadataFrame = tkinter.Frame( - tk, - width=439, - height=99 -) -metadataFrame.place(x=20,y=220) -metadataFrame.columnconfigure(0,weight=10) -metadataFrame.rowconfigure(0,weight=10) -metadataFrame.grid_propagate(False) -metadataFrame.config(bg="#e5eaf0") - -# Metadata text box -metadataInput = tkinter.scrolledtext.ScrolledText( - metadataFrame, - exportselection=0, - height=5, - padx=5, - pady=5 -) -metadataInput.config(font=("Consolas",12)) -metadataInput.grid(row=0,column=0,sticky="nesw",padx=1,pady=1) -metadataInput.config(borderwidth=0) -metadataInput.config(bg="#fbfcfc") -metadataInput["state"] = "disabled" -metadataInput.bind( - "", - lambda e:metadataBoxUI("in") -) -metadataInput.bind( - "", - lambda e:metadataBoxUI("out") -) -# Tkinter's Text() boxes are ugly, so I beautify it manually -def metadataBoxUI(what): - if what=="in": - if metadataInput.cget("bg")=="#ffffff": - metadataFrame.config(bg="#78a7e5") - else: - metadataFrame.config(bg="#d8ddea") - -# Check box for keeping corrupted or modified output -keep = tkinter.IntVar(tk) -keepBtn = tkinter.ttk.Checkbutton( - tk, - text=strings[10], - variable=keep, - onvalue=1, - offvalue=0, - command=lambda:dummy.focus() -) -keepBtn.place(x=18,y=329) -keepBtn["state"] = "disabled" - -# Check box for securely erasing original files -erase = tkinter.IntVar(tk) -eraseBtn = tkinter.ttk.Checkbutton( - tk, - text=strings[11], - variable=erase, - onvalue=1, - offvalue=0, - command=lambda:dummy.focus() -) -eraseBtn.place(x=18,y=349) -eraseBtn["state"] = "disabled" - -# Check box for enabling Reed-Solomon anti-corruption -rs = tkinter.IntVar(tk) -rsBtn = tkinter.ttk.Checkbutton( - tk, - text=strings[15], - variable=rs, - onvalue=1, - offvalue=0, - command=lambda:dummy.focus() -) -rsBtn.place(x=18,y=369) -rsBtn["state"] = "disabled" - -# Frame so start and cancel button can fill width -startFrame = tkinter.Frame( - tk, - width=440, - height=29 -) -startFrame.place(x=20,y=402) -startFrame.columnconfigure(0,weight=10) -startFrame.grid_propagate(False) -startFrame.config(background="#ffffff") - -# Start button -startBtn = tkinter.ttk.Button( - startFrame, - text="Start", - command=lambda:Thread(target=work,daemon=True).start() -) -startBtn.grid(row=0,column=0,stick="nesw") -startBtn["state"] = "disabled" - -# Cancel button -cancelBtn = tkinter.ttk.Button( - startFrame, - text="Cancel" -) -cancelBtn.grid(stick="nesw") -cancelBtn.grid(row=0,column=1) -cancelBtn["state"] = "disabled" - -# Progress bar -progress = tkinter.ttk.Progressbar( - tk, - orient=tkinter.HORIZONTAL, - length=440, - mode="determinate" -) -progress.place(x=20,y=439) - -# Status label -statusString = tkinter.StringVar(tk) -statusString.set("Ready.") -status = tkinter.ttk.Label( - tk, - textvariable=statusString -) -status.place(x=20,y=453) - -# Credits -hint = "Created by Evan Su. Click for details and source." -creditsString = tkinter.StringVar(tk) -creditsString.set(hint) -credits = tkinter.ttk.Label( - tk, - textvariable=creditsString, - cursor="hand2" -) -credits.place(x=20,y=480) -source = "https://github.com/HACKERALERT/Picocrypt" -credits.bind("",lambda e:webbrowser.open(source)) -credits["state"] = "disabled" - -# Version -versionString = tkinter.StringVar(tk) -versionString.set("v1.12") -version = tkinter.ttk.Label( - tk, - textvariable=versionString -) -version["state"] = "disabled" -version.place(x=430,y=480) - -# Files have been dragged -def filesDragged(draggedFiles): - global inputFile,rs128,onlyFiles,mode,onlyFolders,allFiles +# 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) - # Use try to catch file errors + + # Use try to handle errors try: - # Create lists to track files dragged - onlyFiles = [] - onlyFolders = [] + # Create list of input files allFiles = [] - tmpName = "" - tmp = [i for i in draggedFiles] + files = [] + draggedFolderPaths = [] + suffix = "" + 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) @@ -453,436 +141,659 @@ def filesDragged(draggedFiles): tmpName = "" else: if i==" " and not within: - if tmpName: + if tmpName!="": res.append(tmpName) tmpName = "" else: tmpName += i if tmpName: res.append(tmpName) - - # Check each item dragged by user + + allFiles = [] + files = [] + + # Check each thing dragged by user for i in res: - # If it's a directory, recursively add all files + # If there is a directory, recursively add all files to 'allFiles' if isdir(i): - onlyFolders.append(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 'onlyFiles' + # Just a file, add it to files else: - onlyFiles.append(i) - - # If there's only one file, set it as 'inputFile' - if len(onlyFiles)==1 and not len(allFiles): - inputFile = onlyFiles[0] - onlyFiles = [] + 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 inputFile.endswith(".pcv"): - mode = "decrypt" suffix = " (will decrypt)" - - # Read file metadata fin = open(inputFile,"rb") - fin.read(129) - metadataLength = fin.read(138) - metadataLength = bytes(rs128.decode(metadataLength)[0]) - metadataLength = metadataLength.replace(b"+",b"") - metadata = fin.read(int(metadataLength.decode("utf-8"))) - metadata = bytes(rs128.decode(metadata)[0]).decode("utf-8") - metadataString.set("File metadata (read only):") - metadataInput["state"] = "normal" - metadataInput.delete("1.0",tkinter.END) - metadataInput.insert("1.0",metadata) - metadataInput["state"] = "disabled" + + # Read file metadata (a little complex) + tmp = fin.read(139) + reedsolo = False + if tmp[0]==43: + reedsolo = True + tmp = tmp[1:] + else: + tmp = tmp[:-1] + tmp = bytes(headerRsc.decode(tmp)[0]) + tmp = tmp.replace(b"+",b"") + tmp = int(tmp.decode("utf-8")) + if not reedsolo: + fin.seek(138) + ad = fin.read(tmp) + try: + ad = bytes(headerRsc.decode(ad)[0]) + except ReedSolomonError: + ad = b"Error decoding file metadata." + ad = ad.decode("utf-8") fin.close() - - # Insert filename into output box - outputFrame.config(width=440) - outputInput["state"] = "normal" - outputInput.delete(0,tkinter.END) - outputInput.insert(0,inputFile[:-4]) - - # Update UI - setDecryptionUI() - else: - mode = "encrypt" + + # Insert the metadata into its text box + adArea["state"] = "normal" + adArea.delete("1.0",tkinter.END) + adArea.insert("1.0",ad) + adArea["state"] = "disabled" # Update UI - setEncryptionUI() - - # Update output box with appropriate name - if inputFile: - outputInput.insert(0,inputFile) - else: - if onlyFiles: - tmp = Path(onlyFiles[0]).parent.absolute() - else: - tmp = Path(onlyFolders[0]).parent.absolute() - tmp = pathJoin(tmp,"Encrypted.zip") - tmp = tmp.replace("\\","/") - outputInput.insert(0,tmp) + adLabelString.set("File metadata (read only):") + keepBtn["state"] = "normal" + eraseBtn["state"] = "disabled" + rsBtn["state"] = "disabled" + cpasswordInput["state"] = "normal" + cpasswordInput.delete(0,"end") + cpasswordInput["state"] = "disabled" + cpasswordString.set("Confirm password (N/A):") + else: + # Update the UI + eraseBtn["state"] = "normal" + keepBtn["state"] = "disabled" + rsBtn["state"] = "normal" + adArea["state"] = "normal" + adArea.delete("1.0",tkinter.END) suffix = " (will encrypt)" - - nFiles = len(onlyFiles) - nFolders = len(onlyFolders) + 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 onlyFiles) and not onlyFolders: + if (allFiles or files) and not draggedFolderPaths: inputString.set(f"{nFiles} files selected (will encrypt).") - elif onlyFolders and not onlyFiles: + elif draggedFolderPaths and not files: inputString.set(f"{nFolders} folder{'s' if nFolders!=1 else ''} selected (will encrypt).") - elif onlyFolders and (allFiles or onlyFiles): + 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) - - # UTF-8 decode error + + # Enable password box, etc. + 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 except UnicodeDecodeError: - statusString.set(strings[20]) + statusString.set(corruptedNotice) progress["value"] = 100 - - # Nothing happened + + # No file(s) selected, do nothing except: - pass - -# Bind drag and drop to window + 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 + +# Clears the selected files +def clearInputs(): + dummy.focus() + resetUI() + +# Allow drag and drop def onDrop(e): global working if not working: - filesDragged(e.data) - clearInput["state"] = "normal" - clearInput.config(cursor="hand2") + inputSelected(e.data) tk.drop_target_register(DND_FILES) tk.dnd_bind("<>",onDrop) -def work(): - global inputFile,outputFile,working,mode,rs13,rs128,reedsolo - global done,stopUpdating,startTime,previousTime,onlyFiles - global onlyFolders,allFiles,reedsoloFixed,reedsoloErrors - disableAllInputs() - dummy.focus() +# Label that displays selected input file +inputString = tkinter.StringVar(tk) +inputString.set("Drag and drop file(s) and folder(s) into this window.") +selectedInput = tkinter.ttk.Label( + tk, + textvariable=inputString +) +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) +passwordString.set("Password:") +passwordLabel = tkinter.ttk.Label( + tk, + textvariable=passwordString +) +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=(445 if platform.system()=="Darwin" else 440), + height=22 +) +passwordFrame.place(x=(17 if platform.system()=="Darwin" else 20),y=66) +passwordFrame.columnconfigure(0,weight=10) +passwordFrame.grid_propagate(False) +# Password input box +passwordInput = tkinter.ttk.Entry( + passwordFrame, + show="\u2022" +) +passwordInput.grid(sticky="nesw") +passwordInput["state"] = "disabled" + +cpasswordString = tkinter.StringVar(tk) +cpasswordString.set("Confirm password:") +cpasswordLabel = tkinter.ttk.Label( + tk, + textvariable=cpasswordString +) +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=(445 if platform.system()=="Darwin" else 440), + height=22 +) +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 +cpasswordInput = tkinter.ttk.Entry( + cpasswordFrame, + show="\u2022" +) +cpasswordInput.grid(sticky="nesw") +cpasswordInput["state"] = "disabled" + +# Start the encryption/decryption process +def start(): + global inputFile,outputFile,password,ad,kept + global working,gMode,headerRsc,allFiles,files + global dragFolderPath + dummy.focus() + reedsolo = False + chunkSize = 2**20 - # Set and get some variables - kept = False - shouldKeep = keep.get()==1 - shouldErase = erase.get()==1 - reedsolo = rs.get()==1 - working = True - stopUpdating = False - headerBroken = False - reedsoloFixed = 0 - reedsoloErrors = 0 - password = passwordInput.get().encode("utf-8") - metadata = metadataInput.get("1.0",tkinter.END).encode("utf-8") - # Decide if encrypting or decrypting - if mode=="encrypt": - outputFile = outputInput.get()+".pcv" + if not inputFile.endswith(".pcv"): + mode = "encrypt" + gMode = "encrypt" + outputFile = inputFile+".pcv" + reedsolo = rs.get()==1 else: - outputFile = outputInput.get() - + mode = "decrypt" + gMode = "decrypt" + # Check if Reed-Solomon was enabled by checking for "+" + test = open(inputFile,"rb") + decider = test.read(1).decode("utf-8") + test.close() + if decider=="+": + reedsolo = True + # Decrypted output is just input file without the extension + outputFile = inputFile[:-4] + + # Check if file already exists (getsize() throws error if file not found) + try: + getsize(outputFile) + force = messagebox.askyesno("Confirmation",overwriteNotice) + dummy.focus() + if force!=1: + return + except: + pass + + # Disable inputs and buttons while encrypting/decrypting + disableAllInputs() + # Make sure passwords match - if passwordInput.get()!=cPasswordInput.get() and mode=="encrypt": - setEncryptionUI() + if passwordInput.get()!=cpasswordInput.get() and mode=="encrypt": + resetEncryptionUI() statusString.set("Passwords don't match.") return - + # Set progress bar indeterminate progress.config(mode="indeterminate") progress.start(15) + statusString.set(rscNotice) - # Compress files together if necessary - if onlyFiles or allFiles: - statusString.set(strings[1]) - tmp = outputFile[:-4] - if onlyFiles: - zfPath = Path(onlyFiles[0]).parent.absolute() + # Create Reed-Solomon object + 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) + zfName = pathJoin(zfPath,tmp+".zip") zf = ZipFile(zfName,"w") for i in allFiles: zf.write(i,i[zfOffset:]) - for i in onlyFiles: + 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 + headerBroken = False + reedsoloFixedCount = 0 + reedsoloErrorCount = 0 + dummy.focus() + password = passwordInput.get().encode("utf-8") + ad = adArea.get("1.0",tkinter.END).encode("utf-8") + wipe = erase.get()==1 + # Open files try: fin = open(inputFile,"rb") except: - setEncryptionUI() - statusString.set(strings[16]) + resetEncryptionUI() + statusString.set("Folder is empty.") return - + + if reedsolo and mode=="decrypt": + # Move pointer one forward + fin.read(1) + fout = open(outputFile,"wb+") + if reedsolo and mode=="encrypt": + # Signal that Reed-Solomon was enabled with a "+" + fout.write(b"+") + + # Generate values for encryption if encrypting if mode=="encrypt": salt = urandom(16) nonce = urandom(24) - fout = open(outputFile,"wb+") - if reedsolo: - fout.write(rs128.encode(b"+")) - else: - fout.write(rs128.encode(b"-")) - metadata = rs128.encode(metadata) - tmp = len(metadata) - tmp = f"{tmp:+<10}" - tmp = rs128.encode(tmp.encode("utf-8")) - - fout.write(tmp) - fout.write(metadata) - fout.write(rs128.encode(salt)) # Argon2 salt - fout.write(rs128.encode(nonce)) # ChaCha20 nonce - fout.write(b"0"*192) # Hash of key - fout.write(b"0"*144) # Poly1305 MAC - fout.write(b"0"*192) # BLAKE2b CRC + # Reed-Solomon-encode metadata + ad = bytes(headerRsc.encode(ad)) + # Write the metadata to output + tmp = str(len(ad)).encode("utf-8") + # Right-pad with "+" + while len(tmp)!=10: + tmp += b"+" + tmp = bytes(headerRsc.encode(tmp)) + fout.write(tmp) # Length of metadata + fout.write(ad) # Metadata (associated data) + + # Write zeros as placeholders, come back to write over it later. + # Note that 128 extra Reed-Solomon bytes are added + fout.write(b"0"*192) # SHA3-512 of encryption key + fout.write(b"0"*192) # CRC of file + fout.write(b"0"*144) # Poly1305 tag + # Reed-Solomon-encode salt and nonce + fout.write(bytes(headerRsc.encode(salt))) # Argon2 salt + fout.write(bytes(headerRsc.encode(nonce))) # ChaCha20 nonce + + # If decrypting, read values from file else: - tmp = fin.read(129) - if bytes(rs128.decode(tmp)[0])==b"+": - reedsolo = True - else: - reedsolo = False - - metadataLength = fin.read(138) - metadataLength = bytes(rs128.decode(metadataLength)[0]) - metadataLength = metadataLength.replace(b"+",b"") - fin.read(int(metadataLength.decode("utf-8"))) + # Move past metadata into actual data + tmp = fin.read(138) + if tmp[0]==43: + tmp = tmp[1:]+fin.read(1) + tmp = bytes(headerRsc.decode(tmp)[0]) + tmp = tmp.replace(b"+",b"") + adlen = int(tmp.decode("utf-8")) + fin.read(int(adlen)) + # Read the salt, nonce, etc. + cs = fin.read(192) + crccs = fin.read(192) + digest = fin.read(144) salt = fin.read(144) nonce = fin.read(152) - keycs = fin.read(192) - maccs = fin.read(144) - crccs = fin.read(192) - + # Reed-Solomon-decode each value try: - salt,_,fixed = rs128.decode(salt) - salt = bytes(salt) - reedsoloFixed += len(fixed) + cs = bytes(headerRsc.decode(cs)[0]) except: headerBroken = True + cs = cs[:64] try: - nonce,_,fixed = rs128.decode(nonce) - nonce = bytes(nonce) - reedsoloFixed += len(fixed) + crccs = bytes(headerRsc.decode(crccs)[0]) except: headerBroken = True + crccs = crccs[:64] try: - keycs,_,fixed = rs128.decode(keycs) - keycs = bytes(keycs) - reedsoloFixed += len(fixed) + digest = bytes(headerRsc.decode(digest)[0]) except: headerBroken = True + digest = digest[:16] try: - maccs,_,fixed = rs128.decode(maccs) - maccs = bytes(maccs) - reedsoloFixed += len(fixed) + salt = bytes(headerRsc.decode(salt)[0]) except: headerBroken = True + salt = salt[:16] try: - crccs,_,fixed = rs128.decode(crccs) - crccs = bytes(crccs) - reedsoloFixed += len(fixed) + nonce = bytes(headerRsc.decode(nonce)[0]) except: headerBroken = True - + nonce = nonce[:24] + if headerBroken: - if not shouldKeep: - statusString.set(strings[8]) + if keep.get()!=1: + statusString.set(veryCorruptedNotice) fin.close() fout.close() remove(outputFile) - setDecryptionUI() + # Reset UI + resetDecryptionUI() return else: kept = "badlyCorrupted" - - statusString.set(strings[9]) - + + # Show notice about key derivation + statusString.set(derivingNotice) + + # Derive argon2id key key = hash_secret_raw( password, salt, - time_cost=8, - memory_cost=2**10, - parallelism=8, + time_cost=8, # 8 iterations + memory_cost=2**20, # 2^20 Kibibytes (1GiB) + parallelism=8, # 8 parallel threads hash_len=32, - type=argonType.D + type=Type.ID ) - + + # Key deriving done, set progress bar determinate progress.stop() progress.config(mode="determinate") progress["value"] = 0 - - check = SHA3_512.new(data=key).digest() - + + # Compute hash of derived key + check = sha3_512.new() + check.update(key) + check = check.digest() + + # If decrypting, check if key is correct if mode=="decrypt": - if not compare_digest(check,keycs): + # If key is incorrect... + if not compare_digest(check,cs): if not headerBroken: - statusString.set(strings[2]) + statusString.set(passwordNotice) fin.close() - setDecryptionUI() + fout.close() + remove(outputFile) + # Reset UI + resetDecryptionUI() return - fout = open(outputFile,"wb+") - - crc = BLAKE2b.new(digest_bits=512) + + # Create XChaCha20-Poly1305 object cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce) + # Cyclic redundancy check for file corruption + crc = sha3_512.new() + + # Amount of data encrypted/decrypted, total file size, starting time done = 0 total = getsize(inputFile) + + # If secure wipe enabled, create a wiper object + + # Keep track of time because it flies... startTime = datetime.now() previousTime = datetime.now() - Thread(target=updateStats,daemon=True,args=(total,)).start() + # Continously read file in chunks of 1MB while True: if mode=="decrypt" and reedsolo: + # Read a chunk plus Reed-Solomon recovery bytes piece = fin.read(1104905) else: - piece = fin.read(2**20) - if not piece: - break + piece = fin.read(chunkSize) + # If EOF + if not piece: + if mode=="encrypt": + # Get the cipher MAC tag (Poly1305) + digest = cipher.digest() + fout.flush() + fout.close() + fout = open(outputFile,"r+b") + # Compute the offset and seek to it (unshift "+") + rsOffset = 1 if reedsolo else 0 + fout.seek(138+len(ad)+rsOffset) + # Write hash of key, CRC, and Poly1305 MAC tag + fout.write(bytes(headerRsc.encode(check))) + fout.write(bytes(headerRsc.encode(crc.digest()))) + fout.write(bytes(headerRsc.encode(digest))) + else: + # If decrypting, verify CRC + crcdg = crc.digest() + if not compare_digest(crccs,crcdg): + # File is corrupted + statusString.set(corruptedNotice) + progress["value"] = 100 + fin.close() + fout.close() + # If keep file not checked... + if keep.get()!=1: + remove(outputFile) + # Reset UI + resetDecryptionUI() + del fin,fout,cipher,key + return + else: + if not kept: + kept = "corrupted" + # Next, verify MAC tag (Poly1305) + try: + # Throws ValueError if incorrect Poly1305 + cipher.verify(digest) + except: + if not reedsoloErrorCount and not headerBroken: + # File is modified + statusString.set(modifiedNotice) + progress["value"] = 100 + fin.close() + fout.close() + # If keep file not checked... + if keep.get()!=1: + remove(outputFile) + # Reset UI + resetDecryptionUI() + del fin,fout,cipher,key + return + else: + if not kept: + kept = "modified" + break + + # Encrypt/decrypt chunk and update CRC if mode=="encrypt": + # Encrypt piece data = cipher.encrypt(piece) - if reedsolo: - data = bytes(rs13.encode(data)) + # Update checksum crc.update(data) + if reedsolo: + # Encode using Reed-Solomon if user chooses + data = bytes(rsc.encode(data)) else: - crc.update(piece) + # Basically encrypting but in reverse if reedsolo: try: - data,_,fixed = rs13.decode(piece) + data,_,fixed = rsc.decode(piece) except ReedSolomonError: # File is really corrupted - if not reedsoloErrors and not shouldKeep: - statusString.set(strings[8]) - - if not shouldKeep: + if not reedsoloErrorCount: + if keep.get()!=1: + statusString.set(veryCorruptedNotice) + progress["value"] = 100 + # If keep file not checked... + if keep.get()!=1: fin.close() fout.close() remove(outputFile) - setDecryptionUI() + # Reset UI + resetDecryptionUI() + del fin,fout,cipher,key return - - kept = "badlyCorrupted" - # Attempt to recover badly corrupted data - data = b"" - piece = piece[:-13] - counter = 0 - while True: - # Basically just strip the Reed-Solomon bytes - # and return the original non-encoded data - if counter<1104905: - data += piece[counter:counter+242] - counter += 255 # 255 bytes, 242 original - else: - break - fixed = bytearray() - reedsoloErrors += 1 - - reedsoloFixed += len(fixed) + else: + kept = "badlyCorrupted" + # Attempt to recover badly corrupted data + data = b"" + piece = piece[:-13] + counter = 0 + while True: + # Basically just strip the Reed-Solomon bytes + # and return the original non-encoded data + if counter<1104905: + data += piece[counter:counter+242] + counter += 255 # 255 bytes, 242 original + else: + break + fixed = bytearray() + reedsoloErrorCount += 1 + data = bytes(data) + reedsoloFixedCount += len(fixed) + crc.update(data) data = cipher.decrypt(data) - else: + crc.update(piece) data = cipher.decrypt(piece) - - fout.write(data) - done += 2**20 - - if mode=="encrypt": - fout.flush() - fout.close() - fout = open(outputFile,"r+b") - fout.seek(129+138+len(metadata)+144+152) - fout.write(rs128.encode(check)) - fout.write(rs128.encode(cipher.digest())) - fout.write(rs128.encode(crc.digest())) - else: - if not compare_digest(crccs,crc.digest()): - statusString.set(strings[3]) - progress["value"] = 100 - fin.close() - fout.close() - - if keep.get()!=1: - remove(outputFile) - setDecryptionUI() - return + + # Calculate speed, ETA, etc. + elapsed = (datetime.now()-previousTime).total_seconds() or 0.0001 + sinceStart = (datetime.now()-startTime).total_seconds() or 0.0001 + previousTime = datetime.now() + + percent = done*100/total + progress["value"] = percent + + 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: + # Set blank ETA if just starting + if sinceStart<0.5: + eta = "..." else: - if not kept: - kept = "corrupted" + eta = f"{eta//60}m {eta%60}" + if isinstance(eta,int) or isinstance(eta,float): + if eta<0: + eta = 0 + + # 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) - try: - cipher.verify(maccs) - except: - if not reedsoloErrors and not headerBroken: - # File is modified - statusString.set(modifiedNotice) - progress["value"] = 100 - fin.close() - fout.close() - # If keep file not checked... - if keep.get()!=1: - remove(outputFile) - # Reset UI - setDecryptionUI() - return - else: - if not kept: - kept = "modified" - + # 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() - stopUpdating = True # Securely wipe files as necessary - if shouldErase: - if onlyFolders: - for i in onlyFolders: + if wipe: + if draggedFolderPaths: + for i in draggedFolderPaths: secureWipe(i) - if onlyFiles: - for i in range(len(onlyFiles)): - statusString.set(strings[12]+f" ({i}/{len(onlyFiles)}") - progress["value"] = i/len(onlyFiles) - secureWipe(onlyFiles[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 or onlyFiles: + if allFiles: # Remove temporary zip file if created remove(inputFile) - print(kept,reedsoloFixed) # Show appropriate notice if file corrupted or modified if not kept: statusString.set(f"Completed. (Click here to show output)") + # Show Reed-Solomon stats if it fixed corrupted bytes - if mode=="decrypt" and reedsoloFixed: + if mode=="decrypt" and reedsolo and reedsoloFixedCount: statusString.set( - f"Completed with {reedsoloFixed}"+ - f" bytes fixed. (Click here to show output)" + f"Completed with {reedsoloFixedCount}"+ + f" bytes fixed. (Output: {output})" ) else: if kept=="modified": - statusString.set(strings[7]) + statusString.set(kModifiedNotice) elif kept=="corrupted": - statusString.set(strings[6]) + statusString.set(kCorruptedNotice) else: - statusString.set(strings[8]) + statusString.set(kVeryCorruptedNotice) status.config(cursor="hand2") @@ -898,49 +809,49 @@ def work(): status.bind("", lambda e:showOutput(output) ) - # Reset variables and UI states resetUI() + status["state"] = "normal" inputFile = "" outputFile = "" - allFiles = [] - onlyFolders = [] - onlyFiles = [] + password = "" + ad = "" + kept = False working = False + allFiles = False + dragFolderPath = False + + # Wipe keys for safety + del fin,fout,cipher,key -def updateStats(total): - global startTime,previousTime,done,stopUpdating,reedsolo,reedsoloFixed,reedsoloErrors - while True: - validStatus = ( - statusString.get().startswith("Working") or statusString.get().startswith("Deriving") - ) - if not stopUpdating and validStatus: - elapsed = (datetime.now()-previousTime).total_seconds() or 0.0001 - sinceStart = (datetime.now()-startTime).total_seconds() or 0.0001 - previousTime = datetime.now() - percent = done*100/total - progress["value"] = percent - - speed = (done/sinceStart)/10**6 or 0.0001 - eta = round((total-done)/(speed*10**6)) - - info = f"Working... {min(percent,100):.0f}% at {speed:.2f} MB/s (ETA: {max(eta,0)}s)" +# Wraps the start() function with error handling +def wrapper(): + global working,gMode + # Try start() and handle errors + try: + start() + except: + # Reset UI accordingly - if reedsolo and mode=="decrypt" and reedsoloFixed: - tmp = "s" if reedsoloFixed!=1 else "" - info += f", fixed {reedsoloFixed} corrupted byte{tmp}" - - if reedsolo and mode=="decrypt" and reedsoloErrors: - info += f", {reedsoloErrors} MB unrecoverable" - - statusString.set(info) - sleep(0.05) + if gMode=="decrypt": + resetDecryptionUI() else: - sys.exit(0) - break + resetEncryptionUI() + statusString.set(unknownErrorNotice) + dummy.focus() + 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. +def startWorker(): + thread = Thread(target=wrapper,daemon=True) + thread.start() + +# Securely wipe file def secureWipe(fin): - statusString.set(strings[12]) + statusString.set(erasingNotice) # Check platform, erase accordingly if platform.system()=="Windows": if isdir(fin): @@ -949,13 +860,13 @@ def secureWipe(fin): if dirname(i) not in paths: paths.append(dirname(i)) for i in range(len(paths)): - statusString.set(strings[12]+f" ({i}/{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(strings[12]) + statusString.set(erasingNotice) progress["value"] = 100 system(f'sdelete64.exe "{fin}" -p 4 -nobanner') elif platform.system()=="Darwin": @@ -963,112 +874,251 @@ def secureWipe(fin): else: system(f'shred -uz "{fin}" -n 4') -# Reset UI to state where no files are selected +# 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(): - inputString.set(strings[18]) - inputLabel["state"] = "normal" - clearInput["state"] = "disabled" - clearInput.config(cursor="") - outputLabel["state"] = "disabled" - outputFrame.config(width=440) - outputInput["state"] = "normal" - outputInput.delete(0,"end") - outputInput["state"] = "disabled" - passwordLabel["state"] = "disabled" + 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" - cPasswordString.set("Confirm password:") - cPasswordLabel["state"] = "disabled" - cPasswordInput["state"] = "normal" - cPasswordInput.delete(0,"end") - cPasswordInput["state"] = "disabled" - metadataFrame.config(bg="#e5eaf0") - metadataInput.config(bg="#fbfcfc") - metadataInput.config(fg="#000000") - metadataString.set(strings[0]) - metadataLabel["state"] = "disabled" - metadataInput["state"] = "normal" - metadataInput.delete("1.0",tkinter.END) - metadataInput["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" - startBtn["state"] = "disabled" - cancelBtn["state"] = "disabled" - progress.stop() - progress.config(mode="determinate") - progress["value"] = 0 - dummy.focus() - -# Set UI to encryption state -def setEncryptionUI(): - outputLabel["state"] = "normal" - outputInput["state"] = "normal" - outputFrame.config(width=410) - passwordLabel["state"] = "normal" - passwordInput["state"] = "normal" - cPasswordLabel["state"] = "normal" - cPasswordString.set("Confirm password:") - cPasswordInput["state"] = "normal" - metadataFrame.config(bg="#d8ddea") - metadataInput.config(bg="#ffffff") - metadataInput.config(fg="#000000") - metadataLabel["state"] = "normal" - metadataInput["state"] = "normal" - eraseBtn["state"] = "normal" - rsBtn["state"] = "normal" - startBtn["state"] = "normal" progress.stop() progress.config(mode="determinate") progress["value"] = 0 -# Set UI to decryption state -def setDecryptionUI(): - outputLabel["state"] = "normal" - outputInput["state"] = "normal" - outputFrame.config(width=440) - passwordLabel["state"] = "normal" - passwordInput["state"] = "normal" - cPasswordString.set("Confirm password (N/A):") - metadataFrame.config(bg="#e5eaf0") - metadataInput.config(bg="#fbfcfc") - metadataInput.config(fg="#666666") - metadataString.set(strings[19]) - metadataInput["state"] = "disabled" - keepBtn["state"] = "normal" - startBtn["state"] = "normal" - 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)}"') -# Disable all inputs while encrypting/decrypting -def disableAllInputs(): - clearInput["state"] = "disabled" - outputInput["state"] = "disabled" - passwordInput["state"] = "disabled" - cPasswordInput["state"] = "disabled" - cPasswordString.set("Confirm password:") - metadataFrame.config(bg="#e5eaf0") - metadataInput.config(bg="#fbfcfc") - metadataInput.config(fg="#666666") - metadataInput["state"] = "disabled" - startBtn["state"] = "disabled" - eraseBtn["state"] = "disabled" - keepBtn["state"] = "disabled" - rsBtn["state"] = "disabled" +# ad stands for "associated data"/metadata +adLabelString = tkinter.StringVar(tk) +adLabelString.set(adString) +adLabel = tkinter.ttk.Label( + tk, + textvariable=adLabelString +) +adLabel.place(x=17,y=148) +adLabel.config(background="#ffffff") +adLabel["state"] = "disabled" -def prepareRsc(): - global rs13,rs128 - rs13 = RSCodec(13) - rs128 = RSCodec(128) +# Frame so metadata text box can fill width +adFrame = tkinter.Frame( + tk, + width=440, + height=100 +) +adFrame.place(x=20,y=168) +adFrame.columnconfigure(0,weight=10) +adFrame.grid_propagate(False) + +# Metadata text box +adArea = tkinter.Text( + adFrame, + exportselection=0 +) +adArea.config(font=("Consolas",12)) +adArea.grid(sticky="we") +adArea["state"] = "disabled" + +# Check box for keeping corrupted/modified output +keep = tkinter.IntVar() +keepBtn = tkinter.ttk.Checkbutton( + tk, + text=keepNotice, + variable=keep, + onvalue=1, + offvalue=0, + command=lambda:dummy.focus() +) +keepBtn.place(x=18,y=280) +keepBtn["state"] = "disabled" + +# Check box for securely erasing original file +erase = tkinter.IntVar() +eraseBtn = tkinter.ttk.Checkbutton( + tk, + text=eraseNotice, + variable=erase, + onvalue=1, + offvalue=0, + command=lambda:dummy.focus() +) +eraseBtn.place(x=18,y=300) +eraseBtn["state"] = "disabled" + +# Check box for Reed Solomon +rs = tkinter.IntVar() +rsBtn = tkinter.ttk.Checkbutton( + tk, + text=rsNotice, + variable=rs, + onvalue=1, + offvalue=0, + command=lambda:dummy.focus() +) +rsBtn.place(x=18,y=320) +rsBtn["state"] = "disabled" + +# Frame so start button can fill width +startFrame = tkinter.Frame( + tk, + width=442, + height=24 +) +startFrame.place(x=19,y=350) +startFrame.columnconfigure(0,weight=10) +startFrame.grid_propagate(False) +# Start button +startBtn = tkinter.ttk.Button( + startFrame, + text="Start", + command=startWorker +) +startBtn.grid(sticky="nesw") +startBtn["state"] = "disabled" + +# Progress bar +progress = tkinter.ttk.Progressbar( + tk, + orient=tkinter.HORIZONTAL, + length=440, + mode="determinate" +) +progress.place(x=20,y=378) + +# Status label +statusString = tkinter.StringVar(tk) +statusString.set("Ready.") +status = tkinter.ttk.Label( + tk, + textvariable=statusString +) +status.place(x=17,y=406) +status.config(background="#ffffff") +status["state"] = "disabled" + +# Credits :) +hint = "Created by Evan Su. Click for details and source." +creditsString = tkinter.StringVar(tk) +creditsString.set(hint) +credits = tkinter.ttk.Label( + tk, + textvariable=creditsString, + cursor="hand2" +) +credits["state"] = "disabled" +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.11") +version = tkinter.ttk.Label( + tk, + textvariable=versionString +) +version["state"] = "disabled" +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 +dummy = tkinter.ttk.Button( + tk +) +dummy.place(x=480,y=0) + +# Function to create Reed-Solomon header codec +def createRsc(): + global headerRsc + headerRsc = RSCodec(128) sys.exit(0) + +def prepare(): + if platform.system()=="Windows": + system("sdelete64.exe /accepteula") -# Prepare Reed-Solomon codecs -Thread(target=prepareRsc,daemon=True).start() +# 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() -# Start tkinter -tk.mainloop() +# 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() + sys.exit(0)