#!/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("<Button-1>",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("<KeyRelease>",lambda e:[showStrength(),doPasswordsMatch()])
cPasswordInput.bind("<KeyRelease>",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(
	"<FocusIn>",
	lambda e:metadataBoxUI("in")
)
metadataInput.bind(
	"<FocusOut>",
	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("<Button-1>",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("<Button-1>",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("<Button-1>",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("<<Drop>>",onDrop)
tk.dnd_bind("<<DropEnter>>",onDropEnter)
tk.dnd_bind("<<DropLeave>>",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("<Button-1>",lambda e:showOutput(output.replace("/","\\")))
	else:
		status.bind("<Button-1>",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)
