#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Picocrypt v1.12.1 Copyright (c) Evan Su (https://evansu.cc) Released under a GNU GPL v3 License https://github.com/HACKERALERT/Picocrypt ~ In cryptography we trust ~ """ # Import dependencies from threading import Thread from datetime import datetime,timedelta from argon2.low_level import hash_secret_raw from argon2.low_level import Type as argonType from Crypto.Cipher import ChaCha20_Poly1305 from Crypto.Hash import SHA3_512 from blake3 import blake3 from hmac import compare_digest from creedsolo import RSCodec,ReedSolomonError from os import urandom,fsync,remove,system from os.path import getsize,expanduser,isdir,exists from os.path import basename,dirname,abspath,realpath from os.path import join as pathJoin,split as pathSplit from pathlib import Path from zipfile import ZipFile from shutil import rmtree from tkinterdnd2 import TkinterDnD,DND_FILES from tkinter.filedialog import asksaveasfilename from ttkthemes import ThemedStyle from time import sleep import re import sys import tkinter import tkinter.ttk import tkinter.scrolledtext import webbrowser import platform # Global variables rootDir = dirname(realpath(__file__)) working = False mode = False inputFile = False outputFile = False rs128 = False rs13 = False allFiles = False onlyFolders = False onlyFiles = False startTime = False previousTime = False done = False stopUpdating = False reedsolo = False reedsoloFixed = False reedsoloErrors = False # A list of strings and notices strings = [ "File metadata (optional):", "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?", "The input file's metadata is corrupted.", "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.", "Error. The input file is corrupted so badly that Picocrypt can't decrypt it." ] # Create root window tk = TkinterDnD.Tk() tk.geometry("480x500") tk.title("Picocrypt") tk.resizable(0,0) tk.configure(background="#f5f6f7") # Set "arc" theme ThemedStyle(tk).set_theme("arc") # Disable high DPI on Windows to prevent ugly scaling try: from ctypes import windll windll.shcore.SetProcessDpiAwareness(0) except: pass # Try setting window icon if it exists try: # PyInstaller bundles files in _MEIPASS from sys import _MEIPASS favicon = tkinter.PhotoImage(file=pathJoin(_MEIPASS,"key.png")) tk.iconphoto(False,favicon) except: try: # PyInstaller not used, load from same directory 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) # 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(),statusString.set("Ready.")] ) clearInput.place(x=386,y=13,width=74,height=27) clearInput["state"] = "disabled" # Separator for aesthetics tkinter.Frame( tk, bg="#dfe3ed", height=1 ).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=51) 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=314,y=71) # A frame to allow output box to fill width outputFrame = tkinter.Frame( tk, width=320, height=24 ) outputFrame.place(x=20,y=69) 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" # A "or" label between output box and "Save as" button orString = tkinter.StringVar(tk) orString.set("or") orLabel = tkinter.ttk.Label( tk, textvariable=orString ) orLabel.place(x=356,y=71) orLabel["state"] = "disabled" # Save as custom file def saveAs(): global mode,onlyFiles,onlyFolders dummy.focus() # Get the root directory to ask user to save as if inputFile: saveDir = dirname(inputFile) elif onlyFiles: saveDir = dirname(onlyFiles[0]) else: saveDir = Path(onlyFolders[0]).parent.absolute() # Ask user to save file tmp = asksaveasfilename( initialdir=saveDir, initialfile=( basename(inputFile)[:-4] if mode=="decrypt" else basename(inputFile)+".pcv" ), confirmoverwrite=True ) # If a custom output is chosen, update the output box accordingly if tmp: outputInput.delete(0,tkinter.END) outputInput.insert(0,(tmp if mode=="decrypt" else tmp[:-4])) # Button to allow custom output saveAsBtn = tkinter.ttk.Button( tk, text="Save as", command=saveAs ) saveAsBtn.place(x=386,y=67,width=74,height=27) saveAsBtn["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=103) passwordLabel["state"] = "disabled" # Allow password input to fill width passwordFrame = tkinter.Frame( tk, width=210, height=24 ) passwordFrame.place(x=20,y=121) passwordFrame.columnconfigure(0,weight=10) passwordFrame.grid_propagate(False) # Password input box passwordInput = tkinter.ttk.Entry( passwordFrame, show="•" ) passwordInput.grid(sticky="nesw") passwordInput["state"] = "disabled" # Toggle password visibility def showPassword(e): if passwordInput.cget("show"): passwordInput.config(show="") cPasswordInput.config(show="") else: passwordInput.config(show="•") cPasswordInput.config(show="•") # Eye icon to show password passwordShowString = tkinter.StringVar(tk) passwordShowString.set((" " if platform.system()=="Darwin" else "")+"👁") passwordShow = tkinter.ttk.Label( tk, textvariable=passwordShowString, cursor="hand2", font=("TkDefaultFont",14) ) passwordShow.bind("",showPassword) passwordShow["state"] = "disabled" passwordShow.place( x=(235 if platform.system()=="Windows" else 230), y=(121 if platform.system()=="Windows" else 124) ) # 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=158) cPasswordLabel["state"] = "disabled" # Allow confirm password input to fill width cPasswordFrame = tkinter.Frame( tk, width=210, height=24 ) cPasswordFrame.place(x=20,y=176) cPasswordFrame.columnconfigure(0,weight=10) cPasswordFrame.grid_propagate(False) # Confirm password input box cPasswordInput = tkinter.ttk.Entry( cPasswordFrame, show="•" ) cPasswordInput.grid(sticky="nesw") cPasswordInput["state"] = "disabled" # Show strength of password def showStrength(): global mode if mode=="decrypt": return password = passwordInput.get() containsLetters = any(i.isalpha() for i in password) containsNumbers = any(i.isdigit() for i in password) containsSymbols = any(not i.isalnum() for i in password) longEnough = len(password)>8 if containsLetters and containsNumbers and containsSymbols and longEnough: passwordStrength.config(width=208) passwordStrength.config(bg="#149414") elif containsLetters and containsNumbers and containsSymbols: passwordStrength.config(width=140) passwordStrength.config(bg="#fada52") elif containsLetters and containsNumbers or \ (containsLetters and containsSymbols) or \ (containsNumbers and containsSymbols): passwordStrength.config(width=90) passwordStrength.config(bg="#ff781f") elif not password: passwordStrength.config(width=208) passwordStrength.config(bg="#e5eaf0") else: passwordStrength.config(width=20) passwordStrength.config(bg="#e3242b") # Check if passwords match def doPasswordsMatch(): global mode if mode=="decrypt": if passwordInput.get(): startBtn["state"] = "normal" startBtn.config(cursor="hand2") else: startBtn["state"] = "disabled" startBtn.config(cursor="") else: matches = passwordInput.get()==cPasswordInput.get() # Passwords match if passwordInput.get() and matches: passwordMatchesString.set("✔️") startBtn["state"] = "normal" startBtn.config(cursor="hand2") # Passwords don't match elif passwordInput.get() and not matches: passwordMatchesString.set("❌") startBtn["state"] = "disabled" startBtn.config(cursor="") # Password is empty elif not passwordInput.get(): passwordMatchesString.set("") startBtn["state"] = "disabled" startBtn.config(cursor="") # Bind key releases to appropriate functions passwordInput.bind("",lambda e:[showStrength(),doPasswordsMatch()]) cPasswordInput.bind("",lambda e:doPasswordsMatch()) # Show indicator of password strength passwordStrength = tkinter.Frame( tk, height=2, width=208 ) passwordStrength.config(bg="#e5eaf0") passwordStrength.place(x=21,y=146) # Label that indicates if passwords match passwordMatchesString = tkinter.StringVar(tk) passwordMatches = tkinter.ttk.Label( tk, textvariable=passwordMatchesString ) passwordMatches.place(x=236,y=180) # 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=210) metadataLabel["state"] = "disabled" # Frame so metadata box can fill width metadataFrame = tkinter.Frame( tk, width=235, height=99 ) metadataFrame.place(x=20,y=228) 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, padx=5, pady=5 ) metadataInput.config(font=("Consolas",10)) metadataInput.grid(row=0,column=0,sticky="nesw",padx=1,pady=1) metadataInput.config(borderwidth=0) metadataInput.config(bg="#fbfcfc") metadataInput["state"] = "disabled" # Tkinter's Text() boxes are ugly, so I (painfully) beautify it manually metadataInput.bind( "", lambda e:metadataBoxUI("in") ) metadataInput.bind( "", lambda e:metadataBoxUI("out") ) def metadataBoxUI(what): if what=="in": if metadataInput.cget("bg")=="#ffffff": metadataFrame.config(bg="#5294e2") else: metadataFrame.config(bg="#d8ddea") # Another separator for aesthetics tkinter.Frame( tk, bg="#dfe3ed", width=1 ).place(x=262,y=106,height=221) # 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=17,y=337) keepBtn["state"] = "disabled" # Check box for securely erasing original file(s) 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=17,y=357) 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=17,y=377) rsBtn["state"] = "disabled" # "Reed-Solomon" which links to Wikipedia rsHelpString = tkinter.StringVar(tk) rsHelpString.set("(?)") rsHelp = tkinter.ttk.Label( tk, textvariable=rsHelpString, cursor="hand2", font=("Helvetica",(7 if platform.system()=="Windows" else 9)) ) rsHelp.place( x=(259 if platform.system()=="Windows" else 289), y=(382 if platform.system()=="Windows" else 384) ) rsHelpLink = "https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction" rsHelp.bind("",lambda e:webbrowser.open(rsHelpLink)) # Frame so start and cancel button can fill width startFrame = tkinter.Frame( tk, width=440, height=(29 if platform.system()=="Windows" else 30) ) startFrame.place(x=20,y=410) 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=wrapper,daemon=True).start() ) startBtn.grid(row=0,column=0,stick="nesw") startBtn["state"] = "disabled" # Cancel encrypting/decrypting def cancel(): global working working = False dummy.focus() # Cancel button cancelBtn = tkinter.ttk.Button( startFrame, text="Cancel", command=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=(336 if platform.system()=="Windows" else 304), mode="determinate" ) progress.place(x=30,y=420) # Lift start and cancel button in front of progress bar startFrame.lift() # Status label statusString = tkinter.StringVar(tk) statusString.set("Ready.") status = tkinter.ttk.Label( tk, textvariable=statusString ) status.place(x=20,y=448) # Credits hint = "Created by Evan Su. Click for details and source." creditsString = tkinter.StringVar(tk) creditsString.set(hint) creditsLabel = tkinter.ttk.Label( tk, textvariable=creditsString, cursor="hand2" ) creditsLabel.place(x=20,y=468) source = "https://github.com/HACKERALERT/Picocrypt" creditsLabel.bind("",lambda e:webbrowser.open(source)) creditsLabel["state"] = "disabled" # Version versionString = tkinter.StringVar(tk) versionString.set("v1.12.1") version = tkinter.ttk.Label( tk, textvariable=versionString ) version["state"] = "disabled" version.place(x=(430 if platform.system()=="Windows" else 423),y=468) # Drag files indicator window prompt = tkinter.Frame(tk) prompt.config(bg="#f5f6f7") promptString = tkinter.StringVar(tk) promptString.set("Drag and drop file(s) and folder(s) here.") promptLabel = tkinter.ttk.Label( prompt, textvariable=promptString ) promptLabel.place(x=135,y=311) promptIconHor = tkinter.Frame( prompt, bg="#6f737d", height=4 ) promptIconHor.place(x=208,y=261,width=64) promptIconVer = tkinter.Frame( prompt, bg="#6f737d", width=4 ) promptIconVer.place(x=238,y=231,height=64) # Ask user to confirm overwrite confirmOverwrite = tkinter.Frame(tk) confirmOverwrite.config(bg="#f5f6f7") confirmOverwriteString = tkinter.StringVar(tk) confirmOverwriteString.set(strings[13]) confirmOverwriteLabel = tkinter.ttk.Label( confirmOverwrite, textvariable=confirmOverwriteString ) confirmOverwriteLabel.place(x=91,y=200) confirmOverwriteNo = tkinter.ttk.Button( confirmOverwrite, text="No", cursor="hand2", command=lambda:confirmOverwrite.pack_forget() ) confirmOverwriteNo.place(x=147,y=245) # Start encryption/decryption after user confirms def overwriteConfirmed(): confirmOverwrite.pack_forget() Thread(target=wrapper,daemon=True,args=(True,)).start() confirmOverwriteYes = tkinter.ttk.Button( confirmOverwrite, text="Yes", cursor="hand2", command=overwriteConfirmed ) confirmOverwriteYes.place(x=249,y=245) # Files have been dragged def filesDragged(draggedFiles): global inputFile,rs128,onlyFiles,mode,onlyFolders,allFiles resetUI() status.config(cursor="") status.bind("",lambda e:None) # Use try to catch file errors try: # Create lists to track files dragged onlyFiles = [] onlyFolders = [] allFiles = [] tmpName = "" tmp = [i for i in draggedFiles] res = [] within = False """ 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) # Check each item dragged by user for i in res: # If it's a directory, recursively add all files if isdir(i): onlyFolders.append(i) tmp = Path(i).rglob("*") for p in tmp: allFiles.append(abspath(p)) # Just a file, add it to 'onlyFiles' 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 = [] 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) metadataString.set("File metadata (read only):") metadataInput["state"] = "normal" metadataInput.delete("1.0",tkinter.END) try: 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") metadataInput.insert("1.0",metadata) except: # Metadata is corrupted, tell user metadataInput.insert("1.0",strings[14]) metadataInput["state"] = "disabled" 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() startBtn["state"] = "disabled" startBtn.config(cursor="") else: mode = "encrypt" # Update UI setEncryptionUI() startBtn["state"] = "disabled" startBtn.config(cursor="") # 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("\\","/") inputFile = tmp outputInput.insert(0,tmp) suffix = " (will encrypt)" nFiles = len(onlyFiles) nFolders = len(onlyFolders) # Show selected file(s) and folder(s) if (allFiles or onlyFiles) and not onlyFolders: inputString.set(f"{nFiles} files selected (will encrypt).") elif onlyFolders and not onlyFiles: inputString.set(f"{nFolders} folder{'s' if nFolders!=1 else ''} selected (will encrypt).") elif onlyFolders and (allFiles or onlyFiles): 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) prompt.pack_forget() statusString.set("Ready.") # UTF-8 decode error except UnicodeDecodeError: statusString.set(strings[20]) # Nothing happened except: pass # Bind drag and drop to window def onDrop(e): global working if not working: filesDragged(e.data) clearInput["state"] = "normal" clearInput.config(cursor="hand2") def onDropEnter(e): prompt.pack(expand=1,fill=tkinter.BOTH) prompt.lift() def onDropLeave(e): prompt.pack_forget() # Bind functions to window tk.drop_target_register(DND_FILES) tk.dnd_bind("<>",onDrop) tk.dnd_bind("<>",onDropEnter) tk.dnd_bind("<>",onDropLeave) # Main encryption/decryption function def work(): global inputFile,outputFile,working,mode,rs13,rs128,reedsolo global done,stopUpdating,startTime,previousTime,onlyFiles global onlyFolders,allFiles,reedsoloFixed,reedsoloErrors disableAllInputs() dummy.focus() # 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") cancelBtn["state"] = "normal" cancelBtn.config(cursor="hand2") # Decide if encrypting or decrypting if mode=="encrypt": outputFile = outputInput.get()+".pcv" else: outputFile = outputInput.get() # Set progress bar indeterminate progress.config(mode="indeterminate") progress.start(15) # Compress files together if necessary if onlyFiles or allFiles: statusString.set(strings[1]) tmp = outputFile[:-4] if onlyFiles: zfPath = Path(onlyFiles[0]).parent.absolute() else: zfPath = Path(dirname(allFiles[0])).parent.absolute() zfOffset = len(str(zfPath)) zfName = pathJoin(zfPath,tmp) zf = ZipFile(zfName,"w") for i in allFiles: zf.write(i,i[zfOffset:]) for i in onlyFiles: zf.write(i,pathSplit(i)[1]) zf.close() inputFile = zfName outputFile = zfName+".pcv" outputPath = dirname(outputFile) # Open files try: fin = open(inputFile,"rb") except: setEncryptionUI() statusString.set(strings[16]) return # If encrypting, generate values for encryption if mode=="encrypt": salt = urandom(16) # Argon2 salt nonce = urandom(24) # XChaCha20 nonce fout = open(outputFile,"wb+") # Indicate Reed-Solomon with "+" if reedsolo: fout.write(rs128.encode(b"+")) else: fout.write(rs128.encode(b"-")) # Encode metadata and length of metadata metadata = rs128.encode(metadata) tmp = len(metadata) tmp = f"{tmp:+<10}" tmp = rs128.encode(tmp.encode("utf-8")) # Write to file fout.write(tmp) fout.write(metadata) fout.write(rs128.encode(salt)) # Argon2 salt fout.write(rs128.encode(nonce)) # XChaCha20 nonce fout.write(b"0"*192) # Hash of key fout.write(b"0"*144) # Poly1305 MAC fout.write(b"0"*160) # BLAKE3 CRC # If decrypting, read values from file else: tmp = fin.read(129) try: if bytes(rs128.decode(tmp)[0])==b"+": reedsolo = True else: reedsolo = False except: setDecryptionUI() statusString.set(strings[21]) return metadataLength = fin.read(138) metadataLength = bytes(rs128.decode(metadataLength)[0]) metadataLength = metadataLength.replace(b"+",b"") fin.read(int(metadataLength.decode("utf-8"))) # Read values salt = fin.read(144) nonce = fin.read(152) keycs = fin.read(192) maccs = fin.read(144) crccs = fin.read(160) # Try to decode each value, increase Reed-Solomon errors fixed if needed try: salt,_,fixed = rs128.decode(salt) salt = bytes(salt) reedsoloFixed += len(fixed) except: headerBroken = True salt = salt[:16] try: nonce,_,fixed = rs128.decode(nonce) nonce = bytes(nonce) reedsoloFixed += len(fixed) except: headerBroken = True nonce = nonce[:24] try: keycs,_,fixed = rs128.decode(keycs) keycs = bytes(keycs) reedsoloFixed += len(fixed) except: headerBroken = True keycs = keycs[:64] try: maccs,_,fixed = rs128.decode(maccs) maccs = bytes(maccs) reedsoloFixed += len(fixed) except: headerBroken = True maccs = maccs[:16] try: crccs,_,fixed = rs128.decode(crccs) crccs = bytes(crccs) reedsoloFixed += len(fixed) except: headerBroken = True crccs = crccs[:32] # If the header is broken... if headerBroken: # Stop if user chose not to keep broken output if not shouldKeep: statusString.set(strings[4]) fin.close() try: remove(outputFile) except: pass setDecryptionUI() return else: kept = "badlyCorrupted" statusString.set(strings[9]) # Generate Argon2d key from master password key = hash_secret_raw( password, salt, time_cost=8, memory_cost=2**20, parallelism=8, hash_len=32, type=argonType.D ) # Stop the indeterminate progress bar and set determinate progress.stop() progress.config(mode="determinate") progress["value"] = 0 # Hash of the derived Argon2 key check = SHA3_512.new(data=key).digest() # Check if password is correct if mode=="decrypt": if not compare_digest(check,keycs): # If header isn't broken... if not headerBroken: # Tell user password is incorrect statusString.set(strings[2]) fin.close() setDecryptionUI() return fout = open(outputFile,"wb+") crc = blake3() # Blake3 CRC cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce) # XChaCha20 # Variables for calculating speeds, etc. done = 0 total = getsize(inputFile) startTime = datetime.now() previousTime = datetime.now() # Update progress bar, etc. in another thread Thread(target=updateStats,daemon=True,args=(total,)).start() # Start the encryption/decryption process while True: # Check if cancel button pressed if not working: fin.close() fout.close() remove(outputFile) if mode=="encrypt": setEncryptionUI() else: setDecryptionUI() statusString.set("Operation canceled by user.") dummy.focus() return # Read from file, read extra if Reed-Solomon was enabled if mode=="decrypt" and reedsolo: piece = fin.read(1104905) else: piece = fin.read(2**20) # End of file if not piece: break # Encrypt, etc. if mode=="encrypt": data = cipher.encrypt(piece) crc.update(data) if reedsolo: data = bytes(rs13.encode(data)) # Decrypt, etc. else: if reedsolo: try: data,_,fixed = rs13.decode(piece) except ReedSolomonError: # File is really corrupted if not reedsoloErrors and not shouldKeep: stopUpdating = True statusString.set(strings[4]) fin.close() fout.close() remove(outputFile) setDecryptionUI() return # Attempt to recover badly corrupted data kept = "badlyCorrupted" data = b"" piece = piece[:-13] counter = 0 while True: # Basically just strip off the Reed-Solomon bytes if counter<1104905: data += piece[counter:counter+242] counter += 255 # 242 bytes + 13 Reed-Solomon else: break fixed = bytearray() reedsoloErrors += 1 reedsoloFixed += len(fixed) crc.update(data) data = cipher.decrypt(data) else: crc.update(piece) data = cipher.decrypt(piece) # Write the data, increase the amount done fout.write(data) done += 1104905 if (mode=="decrypt" and reedsolo) else 2**20 # Stop UI updater from overwriting potential messages stopUpdating = True # Encryption is done, write appropriate values to file 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())) # Decryption is done, check for integrity and authenticity else: # File is corrupted if not compare_digest(crccs,crc.digest()): statusString.set(strings[3]) fin.close() fout.close() if keep.get()!=1: remove(outputFile) setDecryptionUI() return else: if not kept: kept = "corrupted" try: cipher.verify(maccs) except: if not reedsoloErrors and not headerBroken: # File is modified statusString.set(strings[5]) 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" # Flush outputs, close files if not kept: fout.flush() fsync(fout.fileno()) fout.close() fin.close() # Securely wipe files as necessary if shouldErase: if onlyFolders: for i in onlyFolders: if not working: break secureWipe(i) if onlyFiles: for i in range(len(onlyFiles)): if not working: break statusString.set(strings[12]+f" ({i}/{len(onlyFiles)})") progress["value"] = i/len(onlyFiles) secureWipe(onlyFiles[i]) secureWipe(inputFile) # Secure wipe not enabled else: # Remove temporary zip file if created if allFiles or onlyFiles: remove(inputFile) # Prevent Unicode nonsense on MacOS arrow = "" if platform.system()=="Darwin" else "🡪" # If user hit 'Cancel' during secure wiping... if not working: fin.close() fout.close() resetUI() statusString.set(f"Secure wipe canceled, but encryption was successful. (Output {arrow})") else: # Show appropriate notice if file corrupted or modified if not kept: statusString.set(f"Completed. (Click here to show output {arrow})") # Show Reed-Solomon stats if it fixed corrupted bytes if mode=="decrypt" and reedsoloFixed: tmp = "s" if reedsoloFixed!=1 else "" statusString.set( f"Completed with {reedsoloFixed} byte{tmp}"+ f" fixed. (Click here to show output {arrow})" ) else: if kept=="modified": statusString.set(strings[7]) elif kept=="corrupted": statusString.set(strings[6]) else: statusString.set(strings[8]) status.config(cursor="hand2") # A little hack to prevent reference nonsense output = "".join([i for i in outputFile]) # Bind the output file to the status label if platform.system()=="Windows": status.bind("",lambda e:showOutput(output.replace("/","\\"))) else: status.bind("",lambda e:showOutput(output)) # Reset variables and UI states resetUI() inputFile = "" outputFile = "" allFiles = [] onlyFolders = [] onlyFiles = [] working = False # Wraps the work() function and prevents overwrites def wrapper(yes=False): global working,mode,outputFile if mode=="encrypt": outputFile = outputInput.get()+".pcv" else: outputFile = outputInput.get() try: getsize(outputFile) if not yes: confirmOverwrite.pack(expand=1,fill=tkinter.BOTH) confirmOverwrite.lift() return except: pass try: work() except: working = False if mode=="encrypt": setEncryptionUI() else: setDecryptionUI() statusString.set(strings[17]) finally: dummy.focus() sys.exit(0) # Update visuals with stats def updateStats(total): global startTime,previousTime,done,stopUpdating,reedsolo,reedsoloFixed,reedsoloErrors,working while True: validStatus = ( statusString.get().startswith("Working") or statusString.get().startswith("Deriving") ) # Make sure to stop updating if needed if not stopUpdating and validStatus and working: # Some basic math to 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 = max(round((total-done)/(speed*10**6)),0) eta = str(timedelta(seconds=min(eta,86399))).zfill(8) info = f"Working... {min(percent,100):.0f}% at {speed:.2f} MB/s (ETA: {eta})" # Show the number of fixed and unrecoverable bytes if reedsolo and mode=="decrypt" and reedsoloFixed: tmp = "s" if reedsoloFixed!=1 else "" info += f", fixed {reedsoloFixed} byte{tmp}" if reedsolo and mode=="decrypt" and reedsoloErrors: info += f", {reedsoloErrors} MB unrecoverable" statusString.set(info) sleep(0.05) else: sys.exit(0) break # Securely wipe file(s) via system internals def secureWipe(fin): global working statusString.set(strings[12]) progress["value"] = 100 # Check platform, erase accordingly if platform.system()=="Windows": # Recursively delete folders 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)): if not working: return statusString.set(strings[12]+f" ({i}/{len(paths)})") progress["value"] = 100*i/len(paths) system(f'cd "{paths[i]}" && "{rootDir}/sdelete64.exe" * -p 4 -nobanner') system(f'cd "{rootDir}"') rmtree(fin) else: system(f'sdelete64.exe "{fin}" -p 4 -nobanner') # MacOS elif platform.system()=="Darwin": system(f'rm -rfP "{fin}"') # Linux else: system(f'shred -uz "{fin}" -n 4') # Show output file in user's file manager 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)}"') # Reset UI to state where no files are selected def resetUI(): global working working = False inputString.set(strings[18]) inputLabel["state"] = "normal" clearInput["state"] = "disabled" clearInput.config(cursor="") outputLabel["state"] = "disabled" saveAsBtn.config(cursor="") saveAsBtn["state"] = "disabled" orLabel.place(x=356,y=71) orLabel["state"] = "disabled" outputFrame.config(width=320) outputInput["state"] = "normal" outputInput.delete(0,"end") outputInput["state"] = "disabled" pcvLabel["state"] = "normal" passwordLabel["state"] = "disabled" passwordInput["state"] = "normal" passwordInput.delete(0,"end") passwordInput["state"] = "disabled" passwordInput.config(show="•") passwordShow["state"] = "disabled" cPasswordString.set("Confirm password:") cPasswordLabel["state"] = "disabled" cPasswordInput["state"] = "normal" cPasswordInput.delete(0,"end") cPasswordInput["state"] = "disabled" cPasswordInput.config(show="•") passwordStrength.config(width=208) passwordStrength.config(bg="#e5eaf0") passwordMatchesString.set("") passwordMatches["state"] = "normal" 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" keep.set(0) keepBtn["state"] = "disabled" erase.set(0) eraseBtn["state"] = "disabled" rs.set(0) rsBtn["state"] = "disabled" startFrame.lift() startBtn["state"] = "disabled" startBtn.config(cursor="") cancelBtn["state"] = "disabled" cancelBtn.config(cursor="") progress.stop() progress.config(mode="determinate") progress["value"] = 0 dummy.focus() # Set UI to encryption state def setEncryptionUI(): global working working = False clearInput["state"] = "normal" clearInput.config(cursor="hand2") saveAsBtn.config(cursor="hand2") saveAsBtn["state"] = "normal" orLabel.place(x=353,y=71) orLabel["state"] = "normal" outputLabel["state"] = "normal" outputInput["state"] = "normal" outputFrame.config(width=290) pcvLabel["state"] = "normal" passwordLabel["state"] = "normal" passwordInput["state"] = "normal" passwordShow["state"] = "normal" showStrength() cPasswordLabel["state"] = "normal" cPasswordString.set("Confirm password:") cPasswordInput["state"] = "normal" passwordMatches["state"] = "normal" metadataFrame.config(bg="#cfd6e6") metadataInput.config(bg="#ffffff") metadataInput.config(fg="#000000") metadataLabel["state"] = "normal" metadataInput["state"] = "normal" eraseBtn["state"] = "normal" rsBtn["state"] = "normal" startFrame.lift() startBtn["state"] = "normal" startBtn.config(cursor="hand2") cancelBtn["state"] = "disabled" cancelBtn.config(cursor="") progress.stop() progress.config(mode="determinate") progress["value"] = 0 # Set UI to decryption state def setDecryptionUI(): global working working = False clearInput["state"] = "normal" clearInput.config(cursor="hand2") saveAsBtn.config(cursor="hand2") saveAsBtn["state"] = "normal" orLabel.place(x=356,y=71) orLabel["state"] = "normal" outputLabel["state"] = "normal" outputInput["state"] = "normal" outputFrame.config(width=320) passwordLabel["state"] = "normal" passwordInput["state"] = "normal" passwordShow["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" startFrame.lift() startBtn["state"] = "normal" startBtn.config(cursor="hand2") cancelBtn["state"] = "disabled" cancelBtn.config(cursor="") progress.stop() progress.config(mode="determinate") progress["value"] = 0 # Disable all inputs while encrypting/decrypting def disableAllInputs(): clearInput["state"] = "disabled" clearInput.config(cursor="") saveAsBtn.config(cursor="") saveAsBtn["state"] = "disabled" orLabel["state"] = "disabled" outputInput["state"] = "disabled" pcvLabel["state"] = "disabled" passwordInput["state"] = "disabled" passwordInput.config(show="•") passwordShow["state"] = "disabled" passwordStrength.config(width=208) passwordStrength.config(bg="#e5eaf0") cPasswordInput["state"] = "disabled" cPasswordInput.config(show="•") cPasswordString.set("Confirm password:") passwordMatches["state"] = "disabled" metadataFrame.config(bg="#e5eaf0") metadataInput.config(bg="#fbfcfc") metadataInput.config(fg="#666666") metadataInput["state"] = "disabled" progress.lift() startBtn["state"] = "disabled" startBtn.config(cursor="") eraseBtn["state"] = "disabled" keepBtn["state"] = "disabled" rsBtn["state"] = "disabled" # Allow window to close if not encrypting/decrypting def onClose(): global working if not working: tk.destroy() # Generate Reed-Solomon codecs def prepare(): global rs13,rs128 rs13 = RSCodec(13) rs128 = RSCodec(128) # Accept sdelete64's EULA if platform.system()=="Windows": system("sdelete64.exe /accepteula") sys.exit(0) # Prepare Reed-Solomon codecs Thread(target=prepare,daemon=True).start() # Bind tkinter close event tk.protocol("WM_DELETE_WINDOW",onClose) # Start tkinter tk.mainloop() sys.exit(0)