package main

/*

Picocrypt v1.20
Copyright (c) Evan Su (https://evansu.cc)
Released under a GNU GPL v3 License
https://github.com/HACKERALERT/Picocrypt

~ In cryptography we trust ~

*/

import (
	_ "embed"

	// Generic
	"archive/zip"
	"bytes"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"hash"
	"image"
	"image/color"
	"image/png"
	"io"
	"io/ioutil"
	"math"
	"math/big"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"runtime/debug"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	// Cryptography
	"crypto/cipher"
	"crypto/hmac"
	"crypto/md5"
	"crypto/rand"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/subtle"

	"github.com/HACKERALERT/serpent"
	"golang.org/x/crypto/argon2"
	"golang.org/x/crypto/blake2b"
	"golang.org/x/crypto/blake2s"
	"golang.org/x/crypto/chacha20"
	"golang.org/x/crypto/hkdf"
	"golang.org/x/crypto/sha3"

	// UI
	"github.com/AllenDang/giu"

	// Reed-Solomon
	"github.com/HACKERALERT/infectious"

	// Helpers
	"github.com/HACKERALERT/clipboard"
	"github.com/HACKERALERT/dialog"
	"github.com/HACKERALERT/jibber_jabber"
	"github.com/HACKERALERT/zxcvbn-go"
)

//go:embed icon.png
var icon []byte

//go:embed font.ttf
var font []byte

//go:embed sdelete64.exe
var sdelete64bytes []byte

//go:embed strings.json
var localeBytes []byte

// Localization
type locale struct {
	iso  string
	data []string
}

var locales []locale
var selectedLocale = "en"
var allLocales = []string{
	"en",
}
var languages = []string{
	"English",
}
var languageSelected int32

// Generic variables
var version = "v1.20"
var dpi float32
var tab = 0
var mode string
var working bool
var recombine bool
var fill float32 = -0.0000001
var sdelete64path string

// Three variables store the input files
var onlyFiles []string
var onlyFolders []string
var allFiles []string

// Input file variables
var inputLabel = "Drop files and folders into this window."
var inputFile string

// Password variables
var password string
var cPassword string
var passwordStrength int
var passwordState = giu.InputTextFlagsPassword
var passwordStateLabel = "Show"

// Password generator variables
var showGenpass = false
var genpassCopy = true
var genpassLength int32 = 32
var genpassUpper = true
var genpassLower = true
var genpassNums = true
var genpassSymbols = true

// Keyfile variables
var keyfile bool
var keyfiles []string
var keyfileOrderMatters bool
var keyfilePrompt = "None selected."
var showKeyfile bool

// Metadata variables
var metadata string
var metadataPrompt = "Metadata:"
var metadataDisabled bool

// Advanced options
var shredTemp bool
var fast bool
var paranoid bool
var reedsolo bool
var deleteWhenDone bool
var split bool
var splitSize string
var splitUnits = []string{
	"KiB",
	"MiB",
	"GiB",
}
var splitSelected int32 = 1
var compress bool
var encryptFilename bool
var keep bool
var kept bool

// Output file variables
var outputFile string

// Status variables
var mainStatus = "Ready."
var mainStatusColor = color.RGBA{0xff, 0xff, 0xff, 0xff}
var popupStatus string

// Progress variables
var progress float32
var progressInfo string
var showProgress bool

// Confirm overwrite variables
var showConfirmation bool

// Reed-Solomon encoders
var rs1, _ = infectious.NewFEC(1, 3) // 1 data shard, 3 total -> 2 parity shards
var rs5, _ = infectious.NewFEC(5, 15)
var rs6, _ = infectious.NewFEC(6, 18)
var rs16, _ = infectious.NewFEC(16, 48)
var rs24, _ = infectious.NewFEC(24, 72)
var rs32, _ = infectious.NewFEC(32, 96)
var rs64, _ = infectious.NewFEC(64, 192)
var rs128, _ = infectious.NewFEC(128, 136)

// File checksum generator variables
var csMd5 string
var csSha1 string
var csSha256 string
var csSha3 string
var csBlake2b string
var csBlake2s string
var csValidate string
var md5Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
var sha1Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
var sha256Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
var sha3Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
var blake2bColor = color.RGBA{0x00, 0x00, 0x00, 0x00}
var blake2sColor = color.RGBA{0x00, 0x00, 0x00, 0x00}
var csProgress float32 = 0
var md5Selected = true
var sha1Selected = true
var sha256Selected = true
var sha3Selected = false
var blake2bSelected = false
var blake2sSelected = false

// Shredder variables
var shredding string = "Ready."
var shredPasses int32 = 4
var stopShredding bool
var shredProgress float32
var shredDone float32
var shredTotal float32
var shredOverlay string

func draw() {
	giu.SingleWindow().Layout(
		giu.Custom(func() {
			pos := giu.GetCursorPos()
			w, _ := giu.CalcTextSize(languages[languageSelected])
			giu.Row(
				giu.Dummy(-w/dpi-34, 0),
				giu.Combo("##language", languages[languageSelected], languages, &languageSelected).OnChange(func() {
					selectedLocale = allLocales[languageSelected]
					shredding = s(shredding)
				}).Size(w/dpi+26),
			).Build()
			giu.SetCursorPos(pos)

			giu.TabBar().TabItems(
				giu.TabItem(s("Encryption")).Layout(
					giu.Custom(func() {
						if giu.IsItemActive() {
							tab = 0
						}
					}),

					giu.Custom(func() {
						if showGenpass {
							giu.PopupModal(s("Generate password:")).
								Flags(giu.WindowFlagsNoMove|giu.WindowFlagsNoResize).Layout(
								giu.Row(
									giu.Label(s("Length:")),
									giu.SliderInt(&genpassLength, 4, 64).Size(fill),
								),
								giu.Checkbox(s("Uppercase"), &genpassUpper),
								giu.Checkbox(s("Lowercase"), &genpassLower),
								giu.Checkbox(s("Numbers"), &genpassNums),
								giu.Checkbox(s("Symbols"), &genpassSymbols),
								giu.Checkbox(s("Copy to clipboard"), &genpassCopy),
								giu.Row(
									giu.Button(s("Cancel")).Size(100, 0).OnClick(func() {
										giu.CloseCurrentPopup()
										showGenpass = false
									}),
									giu.Button(s("Generate")).Size(100, 0).OnClick(func() {
										tmp := genPassword()
										password = tmp
										cPassword = tmp
										passwordStrength = zxcvbn.PasswordStrength(password, nil).Score
										giu.CloseCurrentPopup()
										showGenpass = false
										giu.Update()
									}),
								),
							).Build()
							giu.OpenPopup(s("Generate password:"))
							giu.Update()
						}
					}),

					giu.Custom(func() {
						if showKeyfile {
							giu.PopupModal(s("Manage keyfiles:")).
								Flags(giu.WindowFlagsNoMove|giu.WindowFlagsNoResize).Layout(
								giu.Row(
									giu.Label(s("Drop and drop your keyfiles.")),
								),
								giu.Custom(func() {
									if mode != "decrypt" {
										giu.Checkbox(s("Require correct keyfile order"), &keyfileOrderMatters).Build()
										giu.Tooltip(s("If checked, you will need to drop keyfiles in the correct order.")).Build()
									} else if keyfileOrderMatters {
										giu.Label(s("The correct order of keyfiles is required.")).Build()
									}
								}),

								giu.Custom(func() {
									for _, i := range keyfiles {
										giu.Row(
											giu.Label(filepath.Base(i)),
											giu.Button("Remove").OnClick(func() {
												var tmp []string
												for _, j := range keyfiles {
													if j != i {
														tmp = append(tmp, j)
													}
												}
												keyfiles = tmp
											}),
										).Build()

									}
								}),
								giu.Dummy(0, 200),
								giu.Row(
									giu.Button(s("Clear")).Size(150, 0).OnClick(func() {
										keyfiles = nil
									}),
									giu.Tooltip(s("Remove all keyfiles.")),
									giu.Button(s("Done")).Size(150, 0).OnClick(func() {
										giu.CloseCurrentPopup()
										showKeyfile = false
									}),
								),
							).Build()
							giu.OpenPopup(s("Manage keyfiles:"))
							giu.Update()
						}
					}),

					giu.Custom(func() {
						if showConfirmation {
							giu.PopupModal(s("Warning:")).
								Flags(giu.WindowFlagsNoMove|giu.WindowFlagsNoResize).Layout(
								giu.Label(s("Output already exists. Overwrite?")),
								giu.Row(
									giu.Button(s("No")).Size(100, 0).OnClick(func() {
										giu.CloseCurrentPopup()
										showConfirmation = false
									}),
									giu.Button(s("Yes")).Size(100, 0).OnClick(func() {
										giu.CloseCurrentPopup()
										showConfirmation = false
										showProgress = true
										giu.Update()
										go func() {
											work()
											working = false
											showProgress = false
											debug.FreeOSMemory()
											giu.Update()
										}()
									}),
								),
							).Build()
							giu.OpenPopup(s("Warning:"))
							giu.Update()
						}
					}),

					giu.Custom(func() {
						if showProgress {
							giu.PopupModal(" ").
								Flags(giu.WindowFlagsNoMove|giu.WindowFlagsNoResize|giu.WindowFlagsNoTitleBar).Layout(
								giu.Custom(func() {
									if !working {
										giu.CloseCurrentPopup()
									}
								}),
								giu.Row(
									giu.ProgressBar(progress).Size(280, 0).Overlay(progressInfo),
									giu.Button(s("Cancel")).Size(58, 0).OnClick(func() {
										working = false
									}),
								),
								giu.Label(popupStatus),
							).Build()
							giu.OpenPopup(" ")
							giu.Update()
						}
					}),

					giu.Row(
						giu.Label(inputLabel),
						giu.Custom(func() {
							w, _ := giu.GetAvailableRegion()
							bw, _ := giu.CalcTextSize(s("Clear"))
							p, _ := giu.GetWindowPadding()
							bw += p * 2
							dw := w - bw - p
							giu.Dummy(float32(math.Max(float64(dw/dpi), float64(-bw/dpi-p))), 0).Build()
							giu.SameLine()
							giu.Style().SetDisabled(len(allFiles) == 0 && len(onlyFiles) == 0).To(
								giu.Button(s("Clear")).Size(bw/dpi, 0).OnClick(resetUI),
								giu.Tooltip(s("Clear all input files and reset UI state.")),
							).Build()
						}),
					),

					giu.Separator(),

					giu.Style().SetDisabled(len(allFiles) == 0 && len(onlyFiles) == 0).To(
						giu.Row(
							giu.Label(s("Password:")),
							giu.Dummy(-124, 0),
							giu.Style().SetDisabled(mode == "decrypt" && !keyfile).To(
								giu.Label(s("Keyfiles:")),
							),
						),
						giu.Row(
							giu.Button(s(passwordStateLabel)).Size(54, 0).OnClick(func() {
								if passwordState == giu.InputTextFlagsPassword {
									passwordState = giu.InputTextFlagsNone
									passwordStateLabel = "Hide"
								} else {
									passwordState = giu.InputTextFlagsPassword
									passwordStateLabel = "Show"
								}
							}),

							giu.Button(s("Clear")).Size(54, 0).OnClick(func() {
								password = ""
								cPassword = ""
							}),

							giu.Button(s("Copy")).Size(54, 0).OnClick(func() {
								clipboard.WriteAll(password)
							}),

							giu.Button(s("Paste")).Size(54, 0).OnClick(func() {
								tmp, _ := clipboard.ReadAll()
								password = tmp
								if mode != "decrypt" {
									cPassword = tmp
								}
								passwordStrength = zxcvbn.PasswordStrength(password, nil).Score
								giu.Update()
							}),

							giu.Style().SetDisabled(mode == "decrypt").To(
								giu.Button(s("Create")).Size(54, 0).OnClick(func() {
									showGenpass = true
								}),
							),

							giu.Style().SetDisabled(mode == "decrypt" && !keyfile).To(
								giu.Row(
									giu.Button(s("Edit")).Size(54, 0).OnClick(func() {
										showKeyfile = true
									}),
									giu.Style().SetDisabled(mode == "decrypt").To(
										giu.Button(s("Create")).Size(54, 0).OnClick(func() {
											file, _ := dialog.File().Title(s("Save keyfile as:")).Save()
											if file == "" {
												return
											}
											fout, _ := os.Create(file)
											data := make([]byte, 1048576)
											rand.Read(data)
											fout.Write(data)
											fout.Close()
										}),
									),
								),
							),
						),
						giu.Row(
							giu.InputText(&password).Flags(passwordState).Size(302/dpi).OnChange(func() {
								passwordStrength = zxcvbn.PasswordStrength(password, nil).Score
							}),
							giu.Custom(func() {
								c := giu.GetCanvas()
								p := giu.GetCursorScreenPos()

								var col color.RGBA
								switch passwordStrength {
								case 0:
									col = color.RGBA{0xc8, 0x4c, 0x4b, 0xff}
								case 1:
									col = color.RGBA{0xa9, 0x6b, 0x4b, 0xff}
								case 2:
									col = color.RGBA{0x8a, 0x8a, 0x4b, 0xff}
								case 3:
									col = color.RGBA{0x6b, 0xa9, 0x4b, 0xff}
								case 4:
									col = color.RGBA{0x4c, 0xc8, 0x4b, 0xff}
								}
								if password == "" || mode == "decrypt" {
									col = color.RGBA{0xff, 0xff, 0xff, 0x00}
								}

								path := p.Add(image.Pt(
									int(math.Round(float64(-20*dpi))),
									int(math.Round(float64(12*dpi))),
								))
								c.PathArcTo(path, 6*dpi, -math.Pi/2, float32(passwordStrength+1)/5*2*math.Pi-math.Pi/2, -1)
								c.PathStroke(col, false, 2)
							}),
							giu.Style().SetDisabled(true).To(
								giu.InputText(&keyfilePrompt).Size(fill),
							),
						),
					),

					giu.Style().SetDisabled(password == "").To(
						giu.Row(
							giu.Style().SetDisabled(mode == "decrypt").To(
								giu.Label(s("Confirm password:")),
							),
							giu.Dummy(-124, 0),
							giu.Style().SetDisabled(true).To(
								giu.Label(s("Custom Argon2:")),
							),
						),
					),
					giu.Style().SetDisabled(password == "").To(
						giu.Row(
							giu.Style().SetDisabled(mode == "decrypt").To(
								giu.Row(
									giu.InputText(&cPassword).Flags(passwordState).Size(302/dpi),
									giu.Custom(func() {
										c := giu.GetCanvas()
										p := giu.GetCursorScreenPos()
										col := color.RGBA{0x4c, 0xc8, 0x4b, 0xff}

										if cPassword != password {
											col = color.RGBA{0xc8, 0x4c, 0x4b, 0xff}
										}
										if password == "" || cPassword == "" || mode == "decrypt" {
											col = color.RGBA{0xff, 0xff, 0xff, 0x00}
										}

										path := p.Add(image.Pt(
											int(math.Round(float64(-20*dpi))),
											int(math.Round(float64(12*dpi))),
										))
										c.PathArcTo(path, 6*dpi, 0, 2*math.Pi, -1)
										c.PathStroke(col, false, 2)
									}),
								),
							),
							giu.Style().SetDisabled(true).To(
								giu.Button(s("W.I.P")).Size(fill, 0),
							),
						),
					),

					giu.Dummy(0, 3),
					giu.Separator(),
					giu.Dummy(0, 0),

					giu.Style().SetDisabled(password == "" || (password != cPassword && mode == "encrypt")).To(
						giu.Label(s(metadataPrompt)),
						giu.Style().SetDisabled(metadataDisabled).To(
							giu.InputText(&metadata).Size(fill),
						),

						giu.Label(s("Advanced:")),
						giu.Custom(func() {
							if mode != "decrypt" {
								giu.Row(
									giu.Checkbox(s("Shred temporary files"), &shredTemp),
									giu.Dummy(-221, 0),
									giu.Checkbox(s("Encode with Reed-Solomon"), &reedsolo),
								).Build()
								giu.Row(
									giu.Checkbox(s("Use fast mode"), &fast),
									giu.Dummy(-221, 0),
									giu.Checkbox(s("Delete files when complete"), &deleteWhenDone),
								).Build()
								giu.Row(
									giu.Checkbox(s("Use paranoid mode"), &paranoid),
									giu.Dummy(-221, 0),
									giu.Style().SetDisabled(!(len(allFiles) > 1 || len(onlyFolders) > 0)).To(
										giu.Checkbox(s("Compress files"), &compress),
									),
								).Build()
								giu.Row(
									giu.Style().SetDisabled(true).To(
										giu.Checkbox(s("Encrypt filename (W.I.P)"), &encryptFilename),
									),
									giu.Dummy(-221, 0),
									giu.Checkbox(s("Split every"), &split),
									giu.InputText(&splitSize).Size(55/dpi).Flags(giu.InputTextFlagsCharsHexadecimal).OnChange(func() {
										split = splitSize != ""
									}),
									giu.Combo("##splitter", splitUnits[splitSelected], splitUnits, &splitSelected).Size(52),
								).Build()
							} else {
								giu.Checkbox(s("Keep decrypted output even if it's corrupted or modified"), &keep).Build()
								giu.Checkbox(s("Delete the encrypted files after a successful decryption"), &deleteWhenDone).Build()
								giu.Dummy(0, 52).Build()
							}
						}),

						giu.Label(s("Save output as:")),
						giu.Custom(func() {
							w, _ := giu.GetAvailableRegion()
							bw, _ := giu.CalcTextSize(s("Change"))
							p, _ := giu.GetWindowPadding()
							bw += p * 2
							dw := w - bw - p
							giu.Style().SetDisabled(true).To(
								giu.InputText(&outputFile).Size(dw / dpi / dpi).Flags(giu.InputTextFlagsReadOnly),
							).Build()
							giu.SameLine()
							giu.Button(s("Change")).Size(bw/dpi, 0).OnClick(func() {
								file, _ := dialog.File().Title(s("Save as:")).Save()
								if file == "" {
									return
								}

								if mode == "encrypt" {
									if len(allFiles) > 1 || len(onlyFolders) > 0 {
										file = strings.TrimSuffix(file, ".zip.pcv")
										file = strings.TrimSuffix(file, ".pcv")
										if !strings.HasSuffix(file, ".zip.pcv") {
											file += ".zip.pcv"
										}
									} else {
										file = strings.TrimSuffix(file, ".pcv")
										ind := strings.Index(inputFile, ".")
										file += inputFile[ind:]
										if !strings.HasSuffix(file, ".pcv") {
											file += ".pcv"
										}
									}
								} else {
									ind := strings.Index(file, ".")
									if ind != -1 {
										file = file[:ind]
									}
									if strings.HasSuffix(inputFile, ".zip.pcv") {
										file += ".zip"
									} else {
										tmp := strings.TrimSuffix(filepath.Base(inputFile), ".pcv")
										tmp = tmp[strings.Index(tmp, "."):]
										file += tmp
									}
								}

								outputFile = file
							}).Build()
							giu.Tooltip(s("Save the output with a custom path and name.")).Build()
						}),

						giu.Dummy(0, 2),
						giu.Separator(),
						giu.Dummy(0, 3),

						giu.Button(s("Start")).Size(fill, 34).OnClick(func() {
							if keyfile && keyfiles == nil {
								mainStatus = "Please select your keyfiles."
								mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
								return
							}
							_, err := os.Stat(outputFile)
							if err == nil {
								showConfirmation = true
								giu.Update()
							} else {
								showProgress = true
								giu.Update()
								go func() {
									work()
									working = false
									showProgress = false
									debug.FreeOSMemory()
									giu.Update()
								}()
							}
						}),
						giu.Style().SetColor(giu.StyleColorText, mainStatusColor).To(
							giu.Label(s(mainStatus)),
						),
					),
				),
				giu.TabItem(s("Checksum")).Layout(
					giu.Custom(func() {
						if giu.IsItemActive() {
							tab = 1
						}
					}),
					giu.Label(s("Toggle the hashes you would like to generate and drop a file here.")),

					// MD5
					giu.Custom(func() {
						giu.Checkbox("MD5:", &md5Selected).OnChange(func() {
							csMd5 = ""
						}).Build()
						giu.SameLine()
						w, _ := giu.GetAvailableRegion()
						bw, _ := giu.CalcTextSize(s("Copy"))
						padding, _ := giu.GetWindowPadding()
						bw += 2 * padding
						size := w - bw - padding
						giu.Dummy(size/dpi, 0).Build()
						giu.SameLine()
						giu.Button(s("Copy")+"##md5").Size(bw/dpi, 0).OnClick(func() {
							clipboard.WriteAll(csMd5)
						}).Build()
					}),
					giu.Style().SetColor(giu.StyleColorBorder, md5Color).To(
						giu.Style().SetDisabled(true).To(
							giu.InputText(&csMd5).Size(fill).Flags(giu.InputTextFlagsReadOnly),
						),
					),

					// SHA1
					giu.Custom(func() {
						giu.Checkbox("SHA1:", &sha1Selected).OnChange(func() {
							csSha1 = ""
						}).Build()
						giu.SameLine()
						w, _ := giu.GetAvailableRegion()
						bw, _ := giu.CalcTextSize(s("Copy"))
						padding, _ := giu.GetWindowPadding()
						bw += 2 * padding
						size := w - bw - padding
						giu.Dummy(size/dpi, 0).Build()
						giu.SameLine()
						giu.Button(s("Copy")+"##sha1").Size(bw/dpi, 0).OnClick(func() {
							clipboard.WriteAll(csSha1)
						}).Build()
					}),
					giu.Style().SetColor(giu.StyleColorBorder, sha1Color).To(
						giu.Style().SetDisabled(true).To(
							giu.InputText(&csSha1).Size(fill).Flags(giu.InputTextFlagsReadOnly),
						),
					),

					// SHA256
					giu.Custom(func() {
						giu.Checkbox("SHA256:", &sha256Selected).OnChange(func() {
							csSha256 = ""
						}).Build()
						giu.SameLine()
						w, _ := giu.GetAvailableRegion()
						bw, _ := giu.CalcTextSize(s("Copy"))
						padding, _ := giu.GetWindowPadding()
						bw += 2 * padding
						size := w - bw - padding
						giu.Dummy(size/dpi, 0).Build()
						giu.SameLine()
						giu.Button(s("Copy")+"##sha256").Size(bw/dpi, 0).OnClick(func() {
							clipboard.WriteAll(csSha256)
						}).Build()
					}),
					giu.Style().SetColor(giu.StyleColorBorder, sha256Color).To(
						giu.Style().SetDisabled(true).To(
							giu.InputText(&csSha256).Size(fill).Flags(giu.InputTextFlagsReadOnly),
						),
					),

					// SHA3-256
					giu.Custom(func() {
						giu.Checkbox("SHA3:", &sha3Selected).OnChange(func() {
							csSha3 = ""
						}).Build()
						giu.SameLine()
						w, _ := giu.GetAvailableRegion()
						bw, _ := giu.CalcTextSize(s("Copy"))
						padding, _ := giu.GetWindowPadding()
						bw += 2 * padding
						size := w - bw - padding
						giu.Dummy(size/dpi, 0).Build()
						giu.SameLine()
						giu.Button(s("Copy")+"##sha3").Size(bw/dpi, 0).OnClick(func() {
							clipboard.WriteAll(csSha3)
						}).Build()
					}),
					giu.Style().SetColor(giu.StyleColorBorder, sha3Color).To(
						giu.Style().SetDisabled(true).To(
							giu.InputText(&csSha3).Size(fill).Flags(giu.InputTextFlagsReadOnly),
						),
					),

					// BLAKE2b
					giu.Custom(func() {
						giu.Checkbox("BLAKE2b:", &blake2bSelected).OnChange(func() {
							csBlake2b = ""
						}).Build()
						giu.SameLine()
						w, _ := giu.GetAvailableRegion()
						bw, _ := giu.CalcTextSize(s("Copy"))
						padding, _ := giu.GetWindowPadding()
						bw += 2 * padding
						size := w - bw - padding
						giu.Dummy(size/dpi, 0).Build()
						giu.SameLine()
						giu.Button(s("Copy")+"##blake2b").Size(bw/dpi, 0).OnClick(func() {
							clipboard.WriteAll(csBlake2b)
						}).Build()
					}),
					giu.Style().SetColor(giu.StyleColorBorder, blake2bColor).To(
						giu.Style().SetDisabled(true).To(
							giu.InputText(&csBlake2b).Size(fill).Flags(giu.InputTextFlagsReadOnly),
						),
					),

					// BLAKE2s
					giu.Custom(func() {
						giu.Checkbox("BLAKE2s:", &blake2sSelected).OnChange(func() {
							csBlake2s = ""
						}).Build()
						giu.SameLine()
						w, _ := giu.GetAvailableRegion()
						bw, _ := giu.CalcTextSize(s("Copy"))
						padding, _ := giu.GetWindowPadding()
						bw += 2 * padding
						size := w - bw - padding
						giu.Dummy(size/dpi, 0).Build()
						giu.SameLine()
						giu.Button(s("Copy")+"##blake2s").Size(bw/dpi, 0).OnClick(func() {
							clipboard.WriteAll(csBlake2s)
						}).Build()
					}),
					giu.Style().SetColor(giu.StyleColorBorder, blake2sColor).To(
						giu.Style().SetDisabled(true).To(
							giu.InputText(&csBlake2s).Size(fill).Flags(giu.InputTextFlagsReadOnly),
						),
					),

					giu.Dummy(0, 23),
					// Input entry for validating a checksum
					giu.Row(
						giu.Label(s("Validate a checksum:")),
						giu.Custom(func() {
							bw, _ := giu.CalcTextSize(s("Paste"))
							padding, _ := giu.GetWindowPadding()
							bw += 2 * padding
							giu.Button(s("Paste")).Size(bw/dpi, 0).OnClick(func() {
								tmp, _ := clipboard.ReadAll()
								csValidate = tmp
								md5Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
								sha1Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
								sha256Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
								sha3Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
								blake2bColor = color.RGBA{0x00, 0x00, 0x00, 0x00}
								blake2sColor = color.RGBA{0x00, 0x00, 0x00, 0x00}
								if csValidate == "" {
									return
								}
								csValidate = strings.ToLower(csValidate)
								if csValidate == csMd5 {
									md5Color = color.RGBA{0x00, 0xff, 0x00, 0xff}
								} else if csValidate == csSha1 {
									sha1Color = color.RGBA{0x00, 0xff, 0x00, 0xff}
								} else if csValidate == csSha256 {
									sha256Color = color.RGBA{0x00, 0xff, 0x00, 0xff}
								} else if csValidate == csSha3 {
									sha3Color = color.RGBA{0x00, 0xff, 0x00, 0xff}
								} else if csValidate == csBlake2b {
									blake2bColor = color.RGBA{0x00, 0xff, 0x00, 0xff}
								} else if csValidate == csBlake2s {
									blake2sColor = color.RGBA{0x00, 0xff, 0x00, 0xff}
								}
								giu.Update()
							}).Build()
						}),
						giu.Custom(func() {
							bw, _ := giu.CalcTextSize(s("Paste"))
							padding, _ := giu.GetWindowPadding()
							bw += 2 * padding
							giu.Button(s("Clear")).Size(bw/dpi, 0).OnClick(func() {
								csValidate = ""
								md5Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
								sha1Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
								sha256Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
								sha3Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
								blake2bColor = color.RGBA{0x00, 0x00, 0x00, 0x00}
								blake2sColor = color.RGBA{0x00, 0x00, 0x00, 0x00}
							}).Build()
						}),
					),
					giu.Style().SetDisabled(true).To(
						giu.InputText(&csValidate).Size(fill),
					),

					// Progress bar
					giu.Label(s("Progress:")),
					giu.ProgressBar(csProgress).Size(fill, 0),
				),
				giu.TabItem(s("Shredder")).Layout(
					giu.Custom(func() {
						if giu.IsItemActive() {
							tab = 2
						}
					}),

					giu.Label(s("Drop files and folders here to shred them.")),
					giu.Custom(func() {
						if runtime.GOOS == "darwin" {
							giu.Label(s("Number of passes: Not supported on macOS")).Build()
						} else {
							giu.Row(
								giu.Label(s("Number of passes:")),
								giu.SliderInt(&shredPasses, 1, 32).Size(fill),
							).Build()
						}
					}),
					giu.Dummy(0, -50),
					giu.Custom(func() {
						w, _ := giu.GetAvailableRegion()
						bw, _ := giu.CalcTextSize(s("Cancel"))
						padding, _ := giu.GetWindowPadding()
						bw += 2 * padding
						size := w - bw - padding
						giu.Row(
							giu.ProgressBar(shredProgress).Overlay(shredOverlay).Size(size/dpi, 0),
							giu.Button(s("Cancel")).Size(bw/dpi, 0).OnClick(func() {
								stopShredding = true
								shredding = s("Ready.")
								shredProgress = 0
								shredOverlay = ""
							}),
						).Build()
					}),
					giu.Custom(func() {
						if len(shredding) > 60 {
							shredding = "....." + shredding[len(shredding)-50:]
						}
						giu.Label(shredding).Wrapped(true).Build()
					}),
				),
				giu.TabItem(s("About")).Layout(
					giu.Custom(func() {
						if giu.IsItemActive() {
							tab = 3
						}
					}),
					giu.Label(fmt.Sprintf(s("Picocrypt %s, created by Evan Su (https://evansu.cc/)."), version)),
					giu.Label(s("Released under a GNU GPL v3 License.")),
					giu.Label(s("A warm thank you to all the people listed below.")),
					giu.Label(s("Patrons:")),
					giu.Label("    - Frederick Doe"),
					giu.Label(s("Donators:")),
					giu.Label("    - jp26"),
					giu.Label("    - W.Graham"),
					giu.Label("    - N. Chin"),
					giu.Label("    - Manjot"),
					giu.Label("    - Phil P."),
					giu.Label("    - E. Zahard"),
					giu.Label(s("Translators:")),
					giu.Label("umitseyhan75, digitalblossom, zeeaall, francirc, kurpau"),
					giu.Label(s("Other:")),
					giu.Label("Fuderal, u/greenreddits, u/Tall_Escape, u/NSABackdoors"),
				),
			).Build()
		}),
	)
}

func onDrop(names []string) {
	if tab == 1 {
		go generateChecksums(names[0])
		return
	}
	if tab == 2 {
		go shred(names, true)
		return
	}

	if showKeyfile {
		keyfiles = append(keyfiles, names...)
		tmp := []string{}
		for _, i := range keyfiles {
			duplicate := false
			for _, j := range tmp {
				if i == j {
					duplicate = true
				}
			}
			stat, _ := os.Stat(i)
			if !duplicate && !stat.IsDir() {
				tmp = append(tmp, i)
			}
		}
		keyfiles = tmp
		if len(keyfiles) == 1 {
			keyfilePrompt = s("Using 1 keyfile.")
		}
		keyfilePrompt = fmt.Sprintf(s("Using %d keyfiles."), len(keyfiles))
		return
	}

	// Clear variables
	recombine = false
	onlyFiles = nil
	onlyFolders = nil
	allFiles = nil
	files, folders := 0, 0
	resetUI()

	if len(names) == 1 {
		stat, _ := os.Stat(names[0])
		if stat.IsDir() {
			// Update variables
			mode = "encrypt"
			folders++
			inputLabel = s("1 folder selected.")

			// Add the folder
			onlyFolders = append(onlyFolders, names[0])

			// Set the input and output paths
			inputFile = filepath.Join(filepath.Dir(names[0]), s("Encrypted")) + ".zip"
			outputFile = filepath.Join(filepath.Dir(names[0]), s("Encrypted")) + ".zip.pcv"
		} else {
			files++
			name := filepath.Base(names[0])
			nums := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}
			endsNum := false
			for _, i := range nums {
				if strings.HasSuffix(names[0], i) {
					endsNum = true
				}
			}
			isSplit := strings.Contains(names[0], ".pcv.") && endsNum

			// Decide if encrypting or decrypting
			if strings.HasSuffix(names[0], ".pcv") || isSplit {
				//var err error
				mode = "decrypt"
				inputLabel = name + s(" (will decrypt)")
				metadataPrompt = s("Metadata (read-only):")
				metadataDisabled = true

				if isSplit {
					inputLabel = name + s(" (will recombine and decrypt)")
					ind := strings.Index(names[0], ".pcv")
					names[0] = names[0][:ind+4]
					inputFile = names[0]
					outputFile = names[0][:ind]
					recombine = true
				} else {
					outputFile = names[0][:len(names[0])-4]
				}

				// Open input file in read-only mode
				var fin *os.File
				if isSplit {
					fin, _ = os.Open(names[0] + ".0")
				} else {
					fin, _ = os.Open(names[0])
				}

				// Use regex to test if input is a valid Picocrypt volume
				tmp := make([]byte, 30)
				fin.Read(tmp)
				if string(tmp[:5]) == "v1.13" {
					resetUI()
					mainStatus = "Please use Picocrypt v1.13 to decrypt this file."
					mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
					fin.Close()
					return
				}
				if valid, _ := regexp.Match(`^v\d\.\d{2}.{10}0?\d+`, tmp); !valid && !isSplit {
					resetUI()
					mainStatus = "This doesn't seem to be a Picocrypt volume."
					mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
					fin.Close()
					return
				}
				fin.Seek(0, 0)

				// Read metadata and insert into box
				var err error
				tmp = make([]byte, 15)
				fin.Read(tmp)
				tmp, _ = rsDecode(rs5, tmp)
				if string(tmp) == "v1.14" || string(tmp) == "v1.15" || string(tmp) == "v1.16" {
					resetUI()
					mainStatus = "Please use Picocrypt v1.16 to decrypt this file."
					mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
					fin.Close()
					return
				}
				tmp = make([]byte, 15)
				fin.Read(tmp)
				tmp, err = rsDecode(rs5, tmp)

				if err == nil {
					metadataLength, _ := strconv.Atoi(string(tmp))
					tmp = make([]byte, metadataLength*3)
					fin.Read(tmp)
					metadata = ""

					for i := 0; i < metadataLength*3; i += 3 {
						t, err := rsDecode(rs1, tmp[i:i+3])
						if err != nil {
							metadata = s("Metadata is corrupted.")
							break
						}
						metadata += string(t)
					}
				} else {
					metadata = s("Metadata is corrupted.")
				}

				flags := make([]byte, 18)
				fin.Read(flags)
				fin.Close()
				flags, err = rsDecode(rs6, flags)
				if err != nil {
					mainStatus = "Input file is corrupt and cannot be decrypted."
					mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
					return
				}

				if flags[2] == 1 {
					keyfile = true
					keyfilePrompt = s("Keyfiles required.")
				} else {
					keyfilePrompt = s("Not applicable.")
				}
				if flags[5] == 1 {
					keyfileOrderMatters = true
				}
			} else {
				mode = "encrypt"
				inputLabel = name + s(" (will encrypt)")
				inputFile = names[0]
				outputFile = names[0] + ".pcv"
			}

			// Add the file
			onlyFiles = append(onlyFiles, names[0])
			inputFile = names[0]
		}
	} else {
		mode = "encrypt"

		// There are multiple dropped items, check each one
		for _, name := range names {
			stat, _ := os.Stat(name)

			// Check if item is a file or a directory
			if stat.IsDir() {
				folders++
				onlyFolders = append(onlyFolders, name)
			} else {
				files++
				onlyFiles = append(onlyFiles, name)
				allFiles = append(allFiles, name)
			}
		}

		if folders == 0 {
			inputLabel = fmt.Sprintf(s("%d files selected."), files)
		} else if files == 0 {
			inputLabel = fmt.Sprintf(s("%d folders selected."), files)
		} else {
			if files == 1 && folders > 1 {
				inputLabel = fmt.Sprintf(s("1 file and %d folders selected."), folders)
			} else if folders == 1 && files > 1 {
				inputLabel = fmt.Sprintf(s("%d files and 1 folder selected."), files)
			} else if folders == 1 && files == 1 {
				inputLabel = s("1 file and 1 folder selected.")
			} else {
				inputLabel = fmt.Sprintf(s("%d files and %d folders selected."), files, folders)
			}
		}

		// Set the input and output paths
		inputFile = filepath.Join(filepath.Dir(names[0]), s("Encrypted")) + ".zip"
		outputFile = filepath.Join(filepath.Dir(names[0]), s("Encrypted")) + ".zip.pcv"
	}
	// Recursively add all files to 'allFiles'
	if folders > 0 {
		for _, name := range onlyFolders {
			filepath.Walk(name, func(path string, _ os.FileInfo, _ error) error {
				stat, _ := os.Stat(path)
				if !stat.IsDir() {
					allFiles = append(allFiles, path)
				}
				return nil
			})
		}
	}
}

func work() {
	popupStatus = s("Starting...")
	mainStatus = "Working..."
	mainStatusColor = color.RGBA{0xff, 0xff, 0xff, 0xff}
	working = true
	padded := false

	var salt []byte
	var hkdfSalt []byte
	var serpentSalt []byte
	var nonce []byte
	var keyHash []byte
	var _keyHash []byte
	var keyfileKey []byte
	var keyfileHash []byte = make([]byte, 32)
	var _keyfileHash []byte
	var dataMac []byte

	if mode == "encrypt" {
		if compress {
			popupStatus = s("Compressing files...")
		} else {
			popupStatus = s("Combining files...")
		}

		// "Tar" files together (a .zip file with no compression)
		if len(allFiles) > 1 || len(onlyFolders) > 0 {
			var rootDir string
			if len(onlyFolders) > 0 {
				rootDir = filepath.Dir(onlyFolders[0])
			} else {
				rootDir = filepath.Dir(onlyFiles[0])
			}

			file, err := os.Create(inputFile)
			if err != nil {
				mainStatus = "Access denied by operating system."
				mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
				return
			}

			w := zip.NewWriter(file)
			for i, path := range allFiles {
				if !working {
					w.Close()
					file.Close()
					os.Remove(inputFile)
					mainStatus = "Operation cancelled by user."
					mainStatusColor = color.RGBA{0xff, 0xff, 0xff, 0xff}
					return
				}
				progressInfo = fmt.Sprintf("%d/%d", i, len(allFiles))
				progress = float32(i) / float32(len(allFiles))
				giu.Update()
				if path == inputFile {
					continue
				}

				stat, _ := os.Stat(path)
				header, _ := zip.FileInfoHeader(stat)
				header.Name = strings.TrimPrefix(path, rootDir)

				// Windows requires forward slashes in a .zip file
				if runtime.GOOS == "windows" {
					header.Name = strings.ReplaceAll(header.Name, "\\", "/")
					header.Name = strings.TrimPrefix(header.Name, "/")
				}

				if compress {
					header.Method = zip.Deflate
				} else {
					header.Method = zip.Store
				}
				writer, _ := w.CreateHeader(header)
				file, _ := os.Open(path)
				io.Copy(writer, file)
				file.Close()
			}
			w.Flush()
			w.Close()
			file.Close()
		}
	}

	if recombine {
		popupStatus = s("Recombining file...")
		total := 0

		for {
			_, err := os.Stat(fmt.Sprintf("%s.%d", inputFile, total))
			if err != nil {
				break
			}
			total++
		}

		fout, _ := os.Create(inputFile)
		for i := 0; i < total; i++ {
			fin, _ := os.Open(fmt.Sprintf("%s.%d", inputFile, i))
			for {
				data := make([]byte, 1048576)
				read, err := fin.Read(data)
				if err != nil {
					break
				}
				data = data[:read]
				fout.Write(data)
			}
			fin.Close()
			progressInfo = fmt.Sprintf("%d/%d", i, total)
			progress = float32(i) / float32(total)
			giu.Update()
		}
		fout.Close()
		progressInfo = ""
	}

	stat, _ := os.Stat(inputFile)
	total := stat.Size()
	if mode == "decrypt" {
		total -= 789
	}

	// XChaCha20's max message size is 256 GiB
	if total > 256*1073741824 {
		mainStatus = "Total size is larger than 256 GiB, XChaCha20's limit."
		mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
		return
	}

	// Open input file in read-only mode
	fin, err := os.Open(inputFile)
	if err != nil {
		mainStatus = "Access denied by operating system."
		mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
		return
	}

	var fout *os.File

	// If encrypting, generate values; if decrypting, read values from file
	if mode == "encrypt" {
		popupStatus = s("Generating values...")
		giu.Update()

		var err error
		fout, err = os.Create(outputFile)
		if err != nil {
			mainStatus = "Access denied by operating system."
			mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
			return
		}

		// Generate random cryptography values
		salt = make([]byte, 16)
		hkdfSalt = make([]byte, 32)
		serpentSalt = make([]byte, 16)
		nonce = make([]byte, 24)

		// Write version to file
		fout.Write(rsEncode(rs5, []byte(version)))

		// Encode the length of the metadata with Reed-Solomon
		metadataLength := []byte(fmt.Sprintf("%05d", len(metadata)))
		metadataLength = rsEncode(rs5, metadataLength)

		// Write the length of the metadata to file
		fout.Write(metadataLength)

		// Reed-Solomon-encode the metadata and write to file
		for _, i := range []byte(metadata) {
			fout.Write(rsEncode(rs1, []byte{i}))
		}

		flags := make([]byte, 6)
		if fast {
			flags[0] = 1
		}
		if paranoid {
			flags[1] = 1
		}
		if len(keyfiles) > 0 {
			flags[2] = 1
		}
		if reedsolo {
			flags[3] = 1
		}
		if total%1048576 >= 1048448 {
			flags[4] = 1
		}
		if keyfileOrderMatters {
			flags[5] = 1
		}
		flags = rsEncode(rs6, flags)
		fout.Write(flags)

		// Fill salts and nonce with Go's CSPRNG
		rand.Read(salt)
		rand.Read(hkdfSalt)
		rand.Read(serpentSalt)
		rand.Read(nonce)

		// Encode salt with Reed-Solomon and write to file
		_salt := rsEncode(rs16, salt)
		fout.Write(_salt)

		// Encode HKDF salt with Reed-Solomon and write to file
		_hkdfSalt := rsEncode(rs32, hkdfSalt)
		fout.Write(_hkdfSalt)

		// Encode Serpent salt with Reed-Solomon and write to file
		_serpentSalt := rsEncode(rs16, serpentSalt)
		fout.Write(_serpentSalt)

		// Encode nonce with Reed-Solomon and write to file
		_nonce := rsEncode(rs24, nonce)
		fout.Write(_nonce)

		// Write placeholder for hash of key
		fout.Write(make([]byte, 192))

		// Write placeholder for hash of hash of keyfile
		fout.Write(make([]byte, 96))

		// Write placeholder for HMAC-BLAKE2b/HMAC-SHA3 of file
		fout.Write(make([]byte, 192))
	} else {
		var err1 error
		var err2 error
		var err3 error
		var err4 error
		var err5 error
		var err6 error
		var err7 error
		var err8 error
		var err9 error
		var err10 error

		popupStatus = s("Reading values...")
		giu.Update()

		version := make([]byte, 15)
		fin.Read(version)
		_, err1 = rsDecode(rs5, version)

		tmp := make([]byte, 15)
		fin.Read(tmp)
		tmp, err2 = rsDecode(rs5, tmp)
		metadataLength, _ := strconv.Atoi(string(tmp))

		fin.Read(make([]byte, metadataLength*3))

		flags := make([]byte, 18)
		fin.Read(flags)
		flags, err3 = rsDecode(rs6, flags)
		fast = flags[0] == 1
		paranoid = flags[1] == 1
		reedsolo = flags[3] == 1
		padded = flags[4] == 1

		salt = make([]byte, 48)
		fin.Read(salt)
		salt, err4 = rsDecode(rs16, salt)

		hkdfSalt = make([]byte, 96)
		fin.Read(hkdfSalt)
		hkdfSalt, err5 = rsDecode(rs32, hkdfSalt)

		serpentSalt = make([]byte, 48)
		fin.Read(serpentSalt)
		serpentSalt, err6 = rsDecode(rs16, serpentSalt)

		nonce = make([]byte, 72)
		fin.Read(nonce)
		nonce, err7 = rsDecode(rs24, nonce)

		_keyHash = make([]byte, 192)
		fin.Read(_keyHash)
		_keyHash, err8 = rsDecode(rs64, _keyHash)

		_keyfileHash = make([]byte, 96)
		fin.Read(_keyfileHash)
		_keyfileHash, err9 = rsDecode(rs32, _keyfileHash)

		dataMac = make([]byte, 192)
		fin.Read(dataMac)
		dataMac, err10 = rsDecode(rs64, dataMac)

		// Is there a better way?
		if err1 != nil || err2 != nil || err3 != nil || err4 != nil || err5 != nil ||
			err6 != nil || err7 != nil || err8 != nil || err9 != nil || err10 != nil {
			if keep {
				kept = true
			} else {
				mainStatus = "The header is corrupt and the input file cannot be decrypted."
				mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
				fin.Close()
				return
			}
		}
	}

	popupStatus = s("Deriving key...")
	progress = 0
	progressInfo = ""
	giu.Update()

	// Derive encryption/decryption keys and subkeys
	var key []byte
	if fast {
		key = argon2.IDKey(
			[]byte(password),
			salt,
			4,
			131072,
			4,
			32,
		)
	} else if paranoid {
		key = argon2.IDKey(
			[]byte(password),
			salt,
			8,
			1048576,
			8,
			32,
		)
	} else {
		key = argon2.IDKey(
			[]byte(password),
			salt,
			4,
			1048576,
			4,
			32,
		)
	}

	if !working {
		mainStatus = "Operation cancelled by user."
		mainStatusColor = color.RGBA{0xff, 0xff, 0xff, 0xff}
		if mode == "encrypt" && (len(allFiles) > 1 || len(onlyFolders) > 0) {
			os.Remove(outputFile)
		}
		if recombine {
			os.Remove(inputFile)
		}
		os.Remove(outputFile)
		return
	}

	if len(keyfiles) > 0 || keyfile {
		if keyfileOrderMatters {
			var keysum = sha3.New256()
			for _, path := range keyfiles {
				kin, _ := os.Open(path)
				kstat, _ := os.Stat(path)
				kbytes := make([]byte, kstat.Size())
				kin.Read(kbytes)
				kin.Close()
				keysum.Write(kbytes)
			}
			keyfileKey = keysum.Sum(nil)
			keyfileSha3 := sha3.New256()
			keyfileSha3.Write(keyfileKey)
			keyfileHash = keyfileSha3.Sum(nil)
		} else {
			var keysum []byte
			for _, path := range keyfiles {
				kin, _ := os.Open(path)
				kstat, _ := os.Stat(path)
				kbytes := make([]byte, kstat.Size())
				kin.Read(kbytes)
				kin.Close()
				ksha3 := sha3.New256()
				ksha3.Write(kbytes)
				keyfileKey := ksha3.Sum(nil)
				if keysum == nil {
					keysum = keyfileKey
				} else {
					for i, j := range keyfileKey {
						keysum[i] ^= j
					}
				}
			}
			keyfileKey = keysum
			keyfileSha3 := sha3.New256()
			keyfileSha3.Write(keysum)
			keyfileHash = keyfileSha3.Sum(nil)
		}
	}

	sha3_512 := sha3.New512()
	sha3_512.Write(key)
	keyHash = sha3_512.Sum(nil)

	// Validate password and/or keyfiles
	if mode == "decrypt" {
		keyCorrect := true
		keyfileCorrect := true
		var tmp bool

		keyCorrect = !(subtle.ConstantTimeCompare(keyHash, _keyHash) == 0)
		if keyfile {
			keyfileCorrect = !(subtle.ConstantTimeCompare(keyfileHash, _keyfileHash) == 0)
			tmp = !keyCorrect || !keyfileCorrect
		} else {
			tmp = !keyCorrect
		}

		if tmp || keep {
			if keep {
				kept = true
			} else {
				fin.Close()
				if !keyCorrect {
					mainStatus = "The provided password is incorrect."
				} else {
					if keyfileOrderMatters {
						mainStatus = "Incorrect keyfiles and/or order."
					} else {
						mainStatus = "Incorrect keyfiles."
					}
				}
				mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
				key = nil
				if recombine {
					os.Remove(inputFile)
				}
				return
			}
		}

		var err error
		fout, err = os.Create(outputFile)
		if err != nil {
			mainStatus = "Access denied by operating system."
			mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
			return
		}
	}

	if len(keyfiles) > 0 || keyfile {
		// XOR key and keyfile
		tmp := key
		key = make([]byte, 32)
		for i := range key {
			key[i] = tmp[i] ^ keyfileKey[i]
		}
	}

	done := 0
	counter := 0
	startTime := time.Now()
	chacha20, _ := chacha20.NewUnauthenticatedCipher(key, nonce)

	// Use HKDF-SHA3 to generate a subkey
	var mac hash.Hash
	subkey := make([]byte, 32)
	hkdf := hkdf.New(sha3.New256, key, hkdfSalt, nil)
	hkdf.Read(subkey)
	if fast {
		// Keyed BLAKE2b
		mac, _ = blake2b.New512(subkey)
	} else {
		// HMAC-SHA3
		mac = hmac.New(sha3.New512, subkey)
	}

	// Generate another subkey and cipher (not used unless paranoid mode is checked)
	serpentKey := make([]byte, 32)
	hkdf.Read(serpentKey)
	_serpent, _ := serpent.NewCipher(serpentKey)
	serpentCTR := cipher.NewCTR(_serpent, serpentSalt)

	for {
		if !working {
			mainStatus = "Operation cancelled by user."
			mainStatusColor = color.RGBA{0xff, 0xff, 0xff, 0xff}
			fin.Close()
			fout.Close()
			if mode == "encrypt" && (len(allFiles) > 1 || len(onlyFolders) > 0) {
				os.Remove(outputFile)
			}
			if recombine {
				os.Remove(inputFile)
			}
			os.Remove(outputFile)
			return
		}

		var data []byte
		if mode == "decrypt" && reedsolo {
			data = make([]byte, 1114112)
		} else {
			data = make([]byte, 1048576)
		}

		size, err := fin.Read(data)
		if err != nil {
			break
		}
		data = data[:size]
		_data := make([]byte, len(data))

		// "Actual" encryption is done in the next couple of lines
		if mode == "encrypt" {
			if paranoid {
				serpentCTR.XORKeyStream(_data, data)
				copy(data, _data)
			}

			chacha20.XORKeyStream(_data, data)
			mac.Write(_data)

			if reedsolo {
				copy(data, _data)
				_data = nil
				if len(data) == 1048576 {
					for i := 0; i < 1048576; i += 128 {
						tmp := data[i : i+128]
						tmp = rsEncode(rs128, tmp)
						_data = append(_data, tmp...)
					}
				} else {
					chunks := math.Floor(float64(len(data)) / 128)
					for i := 0; float64(i) < chunks; i++ {
						tmp := data[i*128 : (i+1)*128]
						tmp = rsEncode(rs128, tmp)
						_data = append(_data, tmp...)
					}
					tmp := data[int(chunks*128):]
					_data = append(_data, rsEncode(rs128, pad(tmp))...)
				}
			}
		} else {
			if reedsolo {
				copy(_data, data)
				data = nil
				if len(_data) == 1114112 {
					for i := 0; i < 1114112; i += 136 {
						tmp := _data[i : i+136]
						tmp, err = rsDecode(rs128, tmp)
						if err != nil {
							if keep {
								kept = true
							} else {
								mainStatus = "The input file is too corrupted to decrypt."
								mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
								fin.Close()
								fout.Close()
								broken()
								return
							}
						}
						if i == 1113976 && done+1114112 >= int(total) && padded {
							tmp = unpad(tmp)
						}
						data = append(data, tmp...)
					}
				} else {
					chunks := len(_data)/136 - 1
					for i := 0; i < chunks; i++ {
						tmp := _data[i*136 : (i+1)*136]
						tmp, err = rsDecode(rs128, tmp)
						if err != nil {
							if keep {
								kept = true
							} else {
								mainStatus = "The input file is too corrupted to decrypt."
								mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
								fin.Close()
								fout.Close()
								broken()
								return
							}
						}
						data = append(data, tmp...)
					}
					tmp := _data[int(chunks)*136:]
					tmp, err = rsDecode(rs128, tmp)
					if err != nil {
						if keep {
							kept = true
						} else {
							mainStatus = "The input file is too corrupted to decrypt."
							mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
							fin.Close()
							fout.Close()
							broken()
							return
						}
					}
					tmp = unpad(tmp)
					data = append(data, tmp...)
				}
				_data = make([]byte, len(data))
			}

			mac.Write(data)
			chacha20.XORKeyStream(_data, data)

			if paranoid {
				copy(data, _data)
				serpentCTR.XORKeyStream(_data, data)
			}
		}
		fout.Write(_data)

		// Update stats
		if mode == "decrypt" && reedsolo {
			done += 1114112
		} else {
			done += 1048576
		}
		counter++
		progress = float32(done) / float32(total)
		elapsed := float64(time.Since(startTime)) / math.Pow(10, 9)
		speed := float64(done) / elapsed / math.Pow(10, 6)
		eta := int(math.Floor(float64(total-int64(done)) / (speed * math.Pow(10, 6))))

		if progress > 1 {
			progress = 1
		}

		progressInfo = fmt.Sprintf("%.2f%%", progress*100)
		popupStatus = fmt.Sprintf(s("Working at %.2f MB/s (ETA: %s)"), speed, humanize(eta))
		giu.Update()
	}

	if mode == "encrypt" {
		// Seek back to header and write important data
		fout.Seek(int64(312+len(metadata)*3), 0)
		fout.Write(rsEncode(rs64, keyHash))
		fout.Write(rsEncode(rs32, keyfileHash))
		fout.Write(rsEncode(rs64, mac.Sum(nil)))
	} else {
		// Validate the authenticity of decrypted data
		if subtle.ConstantTimeCompare(mac.Sum(nil), dataMac) == 0 {
			if keep {
				kept = true
			} else {
				fin.Close()
				fout.Close()
				broken()
				return
			}
		}
	}

	fin.Close()
	fout.Close()

	// Split files into chunks
	if split {
		var splitted []string
		popupStatus = s("Splitting file...")
		stat, _ := os.Stat(outputFile)
		size := stat.Size()
		finished := 0
		chunkSize, _ := strconv.Atoi(splitSize)

		// User can choose KiB, MiB, and GiB
		if splitSelected == 0 {
			chunkSize *= 1024
		} else if splitSelected == 1 {
			chunkSize *= 1048576
		} else {
			chunkSize *= 1073741824
		}
		chunks := int(math.Ceil(float64(size) / float64(chunkSize)))
		fin, _ := os.Open(outputFile)

		for i := 0; i < chunks; i++ {
			fout, _ := os.Create(fmt.Sprintf("%s.%d", outputFile, i))
			done := 0
			for {
				data := make([]byte, 1048576)
				read, err := fin.Read(data)
				if err != nil {
					break
				}
				if !working {
					fin.Close()
					fout.Close()
					mainStatus = "Operation cancelled by user."
					mainStatusColor = color.RGBA{0xff, 0xff, 0xff, 0xff}

					// If user cancels, remove the unfinished files
					for _, j := range splitted {
						os.Remove(j)
					}
					os.Remove(fmt.Sprintf("%s.%d", outputFile, i))
					os.Remove(outputFile)
					return
				}
				data = data[:read]
				fout.Write(data)
				done += read
				if done >= chunkSize {
					break
				}
			}
			fout.Close()
			finished++
			splitted = append(splitted, fmt.Sprintf("%s.%d", outputFile, i))
			progress = float32(finished) / float32(chunks)
			progressInfo = fmt.Sprintf("%d/%d", finished, chunks)
			giu.Update()
		}
		fin.Close()
		if shredTemp {
			progressInfo = ""
			popupStatus = s("Shredding temporary files...")
			shred([]string{inputFile + ".pcv"}, false)
		} else {
			os.Remove(inputFile + ".pcv")
		}
	}

	// Remove the temporary file used to combine a splitted Picocrypt volume
	if recombine {
		os.Remove(inputFile)
	}

	// Delete the temporary zip file if user wishes
	if len(allFiles) > 1 || len(onlyFolders) > 0 {
		if shredTemp {
			progressInfo = ""
			popupStatus = s("Shredding temporary files...")
			giu.Update()
			shred([]string{inputFile}, false)
		} else {
			os.Remove(inputFile)
		}
	}

	if deleteWhenDone {
		progressInfo = ""
		popupStatus = s("Deleted files...")
		giu.Update()
		if mode == "decrypt" {
			if recombine {
				total := 0
				for {
					_, err := os.Stat(fmt.Sprintf("%s.%d", inputFile, total))
					if err != nil {
						break
					}
					os.Remove(fmt.Sprintf("%s.%d", inputFile, total))
					total++
				}
			} else {
				os.Remove(inputFile)
			}
		} else {
			for _, i := range onlyFiles {
				os.Remove(i)
			}
			for _, i := range onlyFolders {
				os.RemoveAll(i)
			}
		}
	}

	resetUI()

	// If user chose to keep a corrupted/modified file, let them know
	if kept {
		mainStatus = "The input file is corrupted and/or modified. Please be careful."
		mainStatusColor = color.RGBA{0xff, 0xff, 0x00, 0xff}
	} else {
		mainStatus = "Completed."
		mainStatusColor = color.RGBA{0x00, 0xff, 0x00, 0xff}
	}

	// Clear UI state
	working = false
	kept = false
	key = nil
	popupStatus = s("Ready.")
}

// This function is run if an issue occurs during decryption
func broken() {
	mainStatus = "The input file is either corrupted or intentionally modified."
	mainStatusColor = color.RGBA{0xff, 0x00, 0x00, 0xff}
	if recombine {
		os.Remove(inputFile)
	}
	os.Remove(outputFile)
	giu.Update()
}

// Generate file checksums
func generateChecksums(file string) {
	fin, _ := os.Open(file)

	// Clear UI state
	csMd5 = ""
	csSha1 = ""
	csSha256 = ""
	csSha3 = ""
	csBlake2b = ""
	csBlake2s = ""
	md5Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
	sha1Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
	sha256Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
	sha3Color = color.RGBA{0x00, 0x00, 0x00, 0x00}
	blake2bColor = color.RGBA{0x00, 0x00, 0x00, 0x00}
	blake2sColor = color.RGBA{0x00, 0x00, 0x00, 0x00}
	csValidate = ""

	if md5Selected {
		csMd5 = s("Calculating...")
	}
	if sha1Selected {
		csSha1 = s("Calculating...")
	}
	if sha256Selected {
		csSha256 = s("Calculating...")
	}
	if sha3Selected {
		csSha3 = s("Calculating...")
	}
	if blake2bSelected {
		csBlake2b = s("Calculating...")
	}
	if blake2sSelected {
		csBlake2s = s("Calculating...")
	}

	// Create the checksum objects
	crcMd5 := md5.New()
	crcSha1 := sha1.New()
	crcSha256 := sha256.New()
	crcSha3 := sha3.New256()
	crcBlake2b, _ := blake2b.New256(nil)
	crcBlake2s, _ := blake2s.New256(nil)

	stat, _ := os.Stat(file)
	total := stat.Size()
	var done int64 = 0
	for {
		var data []byte
		_data := make([]byte, 1048576)
		size, err := fin.Read(_data)
		if err != nil {
			break
		}
		data = _data[:size]

		if md5Selected {
			crcMd5.Write(data)
		}
		if sha1Selected {
			crcSha1.Write(data)
		}
		if sha256Selected {
			crcSha256.Write(data)
		}
		if sha3Selected {
			crcSha3.Write(data)
		}
		if blake2bSelected {
			crcBlake2b.Write(data)
		}
		if blake2sSelected {
			crcBlake2s.Write(data)
		}

		done += int64(size)
		csProgress = float32(done) / float32(total)
		giu.Update()
	}
	csProgress = 0
	if md5Selected {
		csMd5 = hex.EncodeToString(crcMd5.Sum(nil))
	}
	if sha1Selected {
		csSha1 = hex.EncodeToString(crcSha1.Sum(nil))
	}
	if sha256Selected {
		csSha256 = hex.EncodeToString(crcSha256.Sum(nil))
	}
	if sha3Selected {
		csSha3 = hex.EncodeToString(crcSha3.Sum(nil))
	}
	if blake2bSelected {
		csBlake2b = hex.EncodeToString(crcBlake2b.Sum(nil))
	}
	if blake2sSelected {
		csBlake2s = hex.EncodeToString(crcBlake2s.Sum(nil))
	}

	fin.Close()
	giu.Update()
}

// Recursively shred all file(s) and folder(s) passed in as 'names'
func shred(names []string, separate bool) {
	stopShredding = false
	shredTotal = 0
	shredDone = 0

	// 'separate' is true if this function is being called from the encryption/decryption tab
	if separate {
		shredOverlay = s("Shredding...")
	}

	// Walk through directories to get the total number of files for statistics
	for _, name := range names {
		filepath.Walk(name, func(path string, _ os.FileInfo, err error) error {
			if err != nil {
				return nil
			}
			stat, _ := os.Stat(path)
			if !stat.IsDir() {
				shredTotal++
			}
			return nil
		})
	}

	for _, name := range names {
		shredding = name

		// Linux and macOS need a command with similar syntax and usage, so they're combined
		if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
			stat, _ := os.Stat(name)
			if stat.IsDir() {
				var coming []string

				// Walk the folder recursively
				filepath.Walk(name, func(path string, _ os.FileInfo, err error) error {
					if err != nil {
						return nil
					}
					if stopShredding {
						return nil
					}
					stat, _ := os.Stat(path)
					if !stat.IsDir() {
						if len(coming) == 128 {
							// Use a WaitGroup to parallelize shredding
							var wg sync.WaitGroup
							for i, j := range coming {
								wg.Add(1)
								go func(wg *sync.WaitGroup, id int, j string) {
									defer wg.Done()
									shredding = j
									var cmd *exec.Cmd
									if runtime.GOOS == "linux" {
										cmd = exec.Command("shred", "-ufvz", "-n", strconv.Itoa(int(shredPasses)), j)
									} else {
										cmd = exec.Command("rm", "-rfP", j)
									}
									cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
									cmd.Run()
									shredDone++
									shredUpdate(separate)
									giu.Update()
								}(&wg, i, j)
							}
							wg.Wait()
							coming = nil
						} else {
							coming = append(coming, path)
						}
					}
					return nil
				})
				for _, i := range coming {
					if stopShredding {
						break
					}
					go func(i string) {
						shredding = i
						var cmd *exec.Cmd
						if runtime.GOOS == "linux" {
							cmd = exec.Command("shred", "-ufvz", "-n", strconv.Itoa(int(shredPasses)), i)
						} else {
							cmd = exec.Command("rm", "-rfP", i)
						}
						cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
						cmd.Run()
						shredDone++
						shredUpdate(separate)
						giu.Update()
					}(i)
				}
				if !stopShredding {
					os.RemoveAll(name)
				}
			} else { // The path is a file, not a directory, so just shred it
				shredding = name
				var cmd *exec.Cmd
				if runtime.GOOS == "linux" {
					cmd = exec.Command("shred", "-ufvz", "-n", strconv.Itoa(int(shredPasses)), name)
				} else {
					cmd = exec.Command("rm", "-rfP", name)
				}
				cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
				cmd.Run()
				shredDone++
				shredUpdate(separate)
			}
		} else if runtime.GOOS == "windows" {
			stat, _ := os.Stat(name)
			if stat.IsDir() {
				// Walk the folder recursively
				filepath.Walk(name, func(path string, _ os.FileInfo, err error) error {
					if err != nil {
						return nil
					}
					stat, _ := os.Stat(path)
					if stat.IsDir() {
						if stopShredding {
							return nil
						}

						t := 0
						files, _ := ioutil.ReadDir(path)
						for _, f := range files {
							if !f.IsDir() {
								t++
							}
						}
						shredDone += float32(t)
						shredUpdate(separate)
						shredding = strings.ReplaceAll(path, "\\", "/") + "/*"
						cmd := exec.Command(sdelete64path, "*", "-p", strconv.Itoa(int(shredPasses)))
						cmd.Dir = path
						cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
						cmd.Run()
						giu.Update()
					}
					return nil
				})

				if !stopShredding {
					// sdelete64 doesn't delete the empty folder, so I'll do it manually
					os.RemoveAll(name)
				}
			} else {
				shredding = name
				cmd := exec.Command(sdelete64path, name, "-p", strconv.Itoa(int(shredPasses)))
				cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
				cmd.Run()
				shredDone++
				shredUpdate(separate)
			}
		}
		giu.Update()
		if stopShredding {
			return
		}
	}

	// Clear UI state
	shredding = s("Completed.")
	shredProgress = 0
	shredOverlay = ""
}

// Update shredding statistics
func shredUpdate(separate bool) {
	if separate {
		shredOverlay = fmt.Sprintf("%d/%d", int(shredDone), int(shredTotal))
		shredProgress = shredDone / shredTotal
	} else {
		popupStatus = fmt.Sprintf("%d/%d", int(shredDone), int(shredTotal))
		progress = shredDone / shredTotal
	}
	giu.Update()
}

// Reset the UI to a clean state with nothing selected or checked
func resetUI() {
	mode = ""
	onlyFiles = nil
	onlyFolders = nil
	allFiles = nil
	inputLabel = s("Drop files and folders into this window.")
	password = ""
	cPassword = ""
	keyfiles = nil
	keyfile = false
	keyfileOrderMatters = false
	keyfilePrompt = s("None selected.")
	metadata = ""
	metadataPrompt = "Metadata:"
	metadataDisabled = false
	shredTemp = false
	keep = false
	reedsolo = false
	split = false
	splitSize = ""
	splitSelected = 1
	fast = false
	deleteWhenDone = false
	paranoid = false
	compress = false
	encryptFilename = false
	inputFile = ""
	outputFile = ""
	progress = 0
	progressInfo = ""
	mainStatus = "Ready."
	mainStatusColor = color.RGBA{0xff, 0xff, 0xff, 0xff}
	giu.Update()
}

// Reed-Solomon encoder
func rsEncode(rs *infectious.FEC, data []byte) []byte {
	var res []byte
	rs.Encode(data, func(s infectious.Share) {
		res = append(res, s.DeepCopy().Data[0])
	})
	return res
}

// Reed-Solomon decoder
func rsDecode(rs *infectious.FEC, data []byte) ([]byte, error) {
	tmp := make([]infectious.Share, rs.Total())
	for i := 0; i < rs.Total(); i++ {
		tmp[i] = infectious.Share{
			Number: i,
			Data:   []byte{data[i]},
		}
	}
	res, err := rs.Decode(nil, tmp)
	if err != nil {
		if rs.Total() == 136 {
			return data[:128], err
		}
		return data[:rs.Total()/3], err
	}
	return res, nil
}

// PKCS7 Pad (for use with Reed-Solomon, not for cryptographic purposes)
func pad(data []byte) []byte {
	padLen := 128 - len(data)%128
	padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
	return append(data, padding...)
}

// PKCS7 Unpad
func unpad(data []byte) []byte {
	length := len(data)
	padLen := int(data[length-1])
	return data[:length-padLen]
}

func genPassword() string {
	chars := ""
	if genpassUpper {
		chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	}
	if genpassLower {
		chars += "abcdefghijklmnopqrstuvwxyz"
	}
	if genpassNums {
		chars += "1234567890"
	}
	if genpassSymbols {
		chars += "-=!@#$^&()_+?"
	}
	if chars == "" {
		return chars
	}
	tmp := make([]byte, genpassLength)
	for i := 0; i < int(genpassLength); i++ {
		j, _ := rand.Int(rand.Reader, new(big.Int).SetUint64(uint64(len(chars))))
		tmp[i] = chars[j.Int64()]
	}
	if genpassCopy {
		clipboard.WriteAll(string(tmp))
	}
	return string(tmp)
}

// Convert seconds to HH:MM:SS
func humanize(seconds int) string {
	hours := int(math.Floor(float64(seconds) / 3600))
	seconds %= 3600
	minutes := int(math.Floor(float64(seconds) / 60))
	seconds %= 60
	hours = int(math.Max(float64(hours), 0))
	minutes = int(math.Max(float64(minutes), 0))
	seconds = int(math.Max(float64(seconds), 0))
	return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
}

func s(term string) string {
	for _, i := range locales {
		if i.iso == selectedLocale {
			for _, j := range locales {
				if j.iso == "en" {
					for k, l := range j.data {
						if l == term {
							return i.data[k]
						}
					}
				}
			}

		}
	}
	return term
}

func main() {
	// Parse locales
	var obj map[string]json.RawMessage
	json.Unmarshal(localeBytes, &obj)
	for i := range obj {
		var tmp []string
		json.Unmarshal(obj[i], &tmp)
		locales = append(locales, locale{
			iso:  i,
			data: tmp,
		})
	}

	// Check system locale
	for _, i := range locales {
		tmp, err := jibber_jabber.DetectIETF()
		if err == nil {
			if strings.HasPrefix(tmp, i.iso) {
				selectedLocale = i.iso
				for j, k := range allLocales {
					if k == i.iso {
						languageSelected = int32(j)
					}
				}
			}
		}
	}

	// Create a temporary file to store sdelete64.exe
	sdelete64, _ := os.CreateTemp("", "sdelete64.*.exe")
	sdelete64path = sdelete64.Name()
	sdelete64.Write(sdelete64bytes)
	sdelete64.Close()
	cmd := exec.Command(sdelete64path, "/accepteula")
	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
	cmd.Run()

	// Set a universal font
	giu.SetDefaultFontFromBytes(font, 18)

	// Create the master window
	window := giu.NewMasterWindow("Picocrypt", 442, 532, giu.MasterWindowFlagsNotResizable)
	dialog.Init()

	// Set window icon
	reader := bytes.NewReader(icon)
	decoded, _ := png.Decode(reader)
	window.SetIcon([]image.Image{decoded})

	// Set callbacks
	window.SetDropCallback(onDrop)
	window.SetCloseCallback(func() bool {
		return !working
	})

	// Set universal DPI
	dpi = giu.Context.GetPlatform().GetContentScale()

	// Start a goroutine to check if a newer version is available
	go func() {
		v, err := http.Get("https://raw.githubusercontent.com/HACKERALERT/Picocrypt/main/internals/version.txt")
		if err == nil {
			body, err := io.ReadAll(v.Body)
			v.Body.Close()
			if err == nil {
				if string(body[:5]) != version {
					mainStatus = "A newer version is available."
					mainStatusColor = color.RGBA{0, 255, 0, 255}
				}
			}
		}
	}()

	// Start the UI
	window.Run(draw)

	// Window closed, clean up
	os.Remove(sdelete64path)
}
