from tkinter import filedialog
from threading import Thread
from datetime import datetime
from argon2.low_level import hash_secret_raw,Type
from Crypto.Cipher import ChaCha20_Poly1305
from hashlib import sha3_512
from secrets import compare_digest
from os import urandom,fsync,remove
from os.path import getsize,expanduser
import sys
import tkinter.scrolledtext as scrolledtext
import tkinter
import tkinter.ttk
import webbrowser

inputFile = ""
outputFile = ""
password = ""
ad = ""
adString = "File metadata (used to store some text along with the file):"
passwordNotice = "Error. The provided password is incorrect."
corruptedNotice = "Error. The input file is corrupted."
modifiedNotice = "Error. The input file has been intentionally modified."
kCorruptedNotice = "The input file is corrupted, but the output has been kept."
kModifiedNotice = "The input file has been intentionally modified, but the output has been kept."
derivingNotice = "Deriving key... (takes a few seconds)"
keepNotice = "Keep decrypted output even if it's corrupted or modified"
kept = False
eraseNotice = "Securely erase and delete original file"
working = False

tk = tkinter.Tk()
tk.geometry("480x390")
tk.title("Picocrypt")
tk.configure(background="#f5f6f7")
tk.resizable(0,0)

favicon = tkinter.PhotoImage(file="./key.png")
tk.iconphoto(False,favicon)

s = tkinter.ttk.Style()
s.configure("TCheckbutton",background="#f5f6f7")

def inputSelected():
    global inputFile
    dummy.focus()
    try:
        suffix = ""
        tmp = filedialog.askopenfilename(
            initialdir=expanduser("~")
        )
        if len(tmp)==0:
            raise Exception("No file selected.")
        inputFile = tmp
        if ".enc" in inputFile.split("/")[-1]:
            suffix = " (will be decrypted)"
            fin = open(inputFile,"rb+")
            adlen = b""
            while True:
                letter = fin.read(1)
                adlen += letter
                if letter==b"|":
                    adlen = adlen[:-1]
                    break
            ad = fin.read(int(adlen.decode("utf-8")))
            fin.close()
            adArea["state"] = "normal"
            adArea.delete("1.0",tkinter.END)
            adArea.insert("1.0",ad.decode("utf-8"))
            adArea["state"] = "disabled"
            adLabelString.set("File metadata (read only):")
            keepBtn["state"] = "normal"
            eraseBtn["state"] = "disabled"
        else:
            eraseBtn["state"] = "normal"
            keepBtn["state"] = "disabled"
            adArea["state"] = "normal"
            adArea.delete("1.0",tkinter.END)
            suffix = " (will be encrypted)"
            adLabelString.set(adString)
        inputString.set(inputFile.split("/")[-1]+suffix)
        passwordInput["state"] = "normal"
        startBtn["state"] = "normal"
        statusString.set("Ready.")
    except:
        pass

selectFileInput = tkinter.ttk.Button(
    tk,
    text="Select file",
    command=inputSelected,
)
selectFileInput.place(x=19,y=20)

inputString = tkinter.StringVar(tk)
inputString.set("Please select a file.")
selectedInput = tkinter.ttk.Label(
    tk,
    textvariable=inputString
)
selectedInput.config(background="#f5f6f7")
selectedInput.place(x=104,y=23)

passwordString = tkinter.StringVar(tk)
passwordString.set("Password:")

passwordLabel = tkinter.ttk.Label(
    tk,
    textvariable=passwordString
)
passwordLabel.place(x=17,y=56)
passwordLabel.config(background="#f5f6f7")

passwordFrame = tkinter.Frame(
    tk,
    width=440,
    height=22
)
passwordFrame.place(x=20,y=76)
passwordFrame.columnconfigure(0,weight=10)
passwordFrame.grid_propagate(False)

passwordInput = tkinter.ttk.Entry(
    passwordFrame
)
passwordInput.grid(sticky="nesw")
passwordInput["state"] = "disabled"

def start():
    global inputFile,outputFile,password,ad,kept,working

    working = True
    dummy.focus()
    password = passwordInput.get().encode("utf-8")
    ad = adArea.get("1.0",tkinter.END).encode("utf-8")
    wipe = erase.get()==1

    passwordInput["state"] = "disabled"
    adArea["state"] = "disabled"
    startBtn["state"] = "disabled"
    keepBtn["state"] = "disabled"

    if ".enc" not in inputFile:
        mode = "encrypt"
        outputFile = inputFile+".enc"
    else:
        mode = "decrypt"
        outputFile = inputFile[:-4]

    fin = open(inputFile,"rb+")
    fout = open(outputFile,"wb+")

    if mode=="encrypt":
        salt = urandom(16)
        nonce = urandom(24)
        fout.write(str(len(ad)).encode("utf-8"))
        fout.write(b"|")
        fout.write(ad)
        fout.write(b"0"*64)
        fout.write(b"0"*64)
        fout.write(b"0"*16)
        fout.write(salt)
        fout.write(nonce)
    else:
        adlen = b""
        while True:
            letter = fin.read(1)
            adlen += letter
            if letter==b"|":
                adlen = adlen[:-1]
                break
        fin.read(int(adlen.decode("utf-8")))
        cs = fin.read(64)
        crccs = fin.read(64)
        digest = fin.read(16)
        salt = fin.read(16)
        nonce = fin.read(24)

    statusString.set(derivingNotice)

    progress.config(mode="indeterminate")
    progress.start(15)

    key = hash_secret_raw(
        password,
        salt,
        time_cost=16,
        memory_cost=1048576,
        parallelism=4,
        hash_len=32,
        type=Type.ID
    )

    progress.stop()
    progress.config(mode="determinate")
    progress["value"] = 0

    check = sha3_512(key).digest()

    if mode=="decrypt":
        if not compare_digest(check,cs):
            statusString.set(passwordNotice)
            fin.close()
            fout.close()
            remove(outputFile)
            passwordInput["state"] = "normal"
            adArea["state"] = "normal"
            startBtn["state"] = "normal"
            keepBtn["state"] = "normal"
            working = False
            del key
            return

    cipher = ChaCha20_Poly1305.new(key=key,nonce=nonce)
    crc = sha3_512()

    done = 0
    total = getsize(inputFile)
    chunkSize = 2**20
    startTime = datetime.now()

    if wipe:
        wiper = open(inputFile,"r+b")
        wiper.seek(0)

    while True:
        piece = fin.read(chunkSize)
        if wipe:
            trash = urandom(len(piece))
            wiper.write(trash)
        if not piece:
            if mode=="encrypt":
                digest = cipher.digest()
                fout.flush()
                fout.close()
                fout = open(outputFile,"r+b")
                fout.seek(len(str(len(ad)))+1+len(ad))
                fout.write(check)
                fout.write(crc.digest())
                fout.write(digest)
            else:
                crcdg = crc.digest()
                if not compare_digest(crccs,crcdg):
                    statusString.set(corruptedNotice)
                    fin.close()
                    fout.close()
                    if keep.get()!=1:
                        remove(outputFile)
                        passwordInput["state"] = "normal"
                        adArea["state"] = "normal"
                        startBtn["state"] = "normal"
                        keepBtn["state"] = "normal"
                        working = False
                        del fin,fout,cipher,key
                        return
                    else:
                        kept = "corrupted"
                try:
                    cipher.verify(digest)
                except:
                    statusString.set(modifiedNotice)
                    fin.close()
                    fout.close()
                    if keep.get()!=1:
                        remove(outputFile)
                        passwordInput["state"] = "normal"
                        adArea["state"] = "normal"
                        startBtn["state"] = "normal"
                        keepBtn["state"] = "normal"
                        working = False
                        del fin,fout,cipher,key
                        return
                    else:
                        kept = "modified"                    
            break
        
        if mode=="encrypt":
            data = cipher.encrypt(piece)
            crc.update(data)
        else:
            crc.update(piece)
            data = cipher.decrypt(piece)

        first = False
        elapsed = (datetime.now()-startTime).total_seconds()
        if elapsed==0:
            elapsed = 0.1**6
        percent = done*100/total
        progress["value"] = percent
        rPercent = round(percent)
        speed = (done/elapsed)/10**6
        if speed==0:
            first = True
            speed = 0.1**6
        rSpeed = round(speed)
        eta = round((total-done)/(speed*10**6))
        if first:
            statusString.set("...% at ... MB/s (ETA: ...s)")
        else:
            statusString.set(f"{rPercent}% at {rSpeed} MB/s (ETA: {eta}s)")
        
        done += chunkSize
        fout.write(data)

    if not kept:
        statusString.set("Completed.")
    else:
        if kept=="modified":
            statusString.set(kModifiedNotice)
        else:
            statusString.set(kCorruptedNotice)
    adArea["state"] = "normal"
    adArea.delete("1.0",tkinter.END)
    adArea["state"] = "disabled"
    startBtn["state"] = "disabled"
    passwordInput["state"] = "normal"
    passwordInput.delete(0,"end")
    passwordInput["state"] = "disabled"
    progress["value"] = 0
    inputString.set("Please select a file.")
    keepBtn["state"] = "normal"
    keep.set(0)
    keepBtn["state"] = "disabled"
    eraseBtn["state"] = "normal"
    erase.set(0)
    eraseBtn["state"] = "disabled"
    if not kept:
        fout.flush()
        fsync(fout.fileno())
    fout.close()
    fin.close()
    if wipe:
        wiper.flush()
        fsync(wiper.fileno())
        wiper.close()
        remove(inputFile)
    inputFile = ""
    outputFile = ""
    password = ""
    ad = ""
    kept = False
    working = False
    del fin,fout,cipher,key
    
def startWorker():
    thread = Thread(target=start,daemon=True)
    thread.start()

adLabelString = tkinter.StringVar(tk)
adLabelString.set(adString)
adLabel = tkinter.ttk.Label(
    tk,
    textvariable=adLabelString
)
adLabel.place(x=17,y=108)
adLabel.config(background="#f5f6f7")

adFrame = tkinter.Frame(
    tk,
    width=440,
    height=100
)
adFrame.place(x=20,y=128)
adFrame.columnconfigure(0,weight=10)
adFrame.grid_propagate(False)

adArea = scrolledtext.ScrolledText(
    adFrame,
    exportselection=0
)
adArea.config(font=("Consolas",12))

adArea.grid(sticky="we")
adArea["state"] = "disabled"

keep = tkinter.IntVar()
keepBtn = tkinter.ttk.Checkbutton(
    tk,
    text=keepNotice,
    variable=keep,
    onvalue=1,
    offvalue=0,
    command=lambda:dummy.focus()
)
keepBtn.place(x=18,y=240)
keepBtn["state"] = "disabled"

erase = tkinter.IntVar()
eraseBtn = tkinter.ttk.Checkbutton(
    tk,
    text=eraseNotice,
    variable=erase,
    onvalue=1,
    offvalue=0,
    command=lambda:dummy.focus()
)
eraseBtn.place(x=18,y=260)
eraseBtn["state"] = "disabled"

startFrame = tkinter.Frame(
    tk,
    width=440,
    height=25
)
startFrame.place(x=19,y=290)
startFrame.columnconfigure(0,weight=10)
startFrame.grid_propagate(False)

startBtn = tkinter.ttk.Button(
    startFrame,
    text="Start",
    command=startWorker
)
startBtn.grid(sticky="nesw")
startBtn["state"] = "disabled"

progress = tkinter.ttk.Progressbar(
    tk,
    orient=tkinter.HORIZONTAL,
    length=440,
    mode="determinate"
)
progress.place(x=20,y=328)

statusString = tkinter.StringVar(tk)
statusString.set("Ready.")
status = tkinter.ttk.Label(
    tk,
    textvariable = statusString
)
status.place(x=17,y=356)
status.config(background="#f5f6f7")

dummy = tkinter.ttk.Button(
    tk
)
dummy.place(x=480,y=0)

def onClose():
    if not working:
        tk.destroy()

if __name__=="__main__":
    tk.protocol("WM_DELETE_WINDOW",onClose)
    tk.mainloop()
    sys.exit(0)
