diff --git a/src/unstable/Picocrypt.py b/src/unstable/Picocrypt.py deleted file mode 100644 index c557b5a..0000000 --- a/src/unstable/Picocrypt.py +++ /dev/null @@ -1,1503 +0,0 @@ -#!/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)