package main

import (
	"archive/zip"
	"bytes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"flag"
	"fmt"
	"hash"
	"io"
	"math"
	"os"
	"os/signal"
	"path/filepath"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"github.com/Picocrypt/infectious"
	"github.com/Picocrypt/serpent"
	"github.com/schollz/progressbar/v3"
	"golang.org/x/crypto/argon2"
	"golang.org/x/crypto/blake2b"
	"golang.org/x/crypto/chacha20"
	"golang.org/x/crypto/hkdf"
	"golang.org/x/crypto/sha3"
	"golang.org/x/term"
)

var f *bool
var k *bool
var p *bool
var r *bool
var mode string

func parse() int {
	flag.Usage = func() {
		fmt.Println("Usage: picocrypt <item1> [<item2> ...]")
		fmt.Println("Items: can be files, folders, or globs")
		fmt.Println("Flags:")
		flag.PrintDefaults()
		os.Exit(1)
	}
	f = flag.Bool("f", false, "(decryption) attempt to fix corruption")
	k = flag.Bool("k", false, "(decryption) keep output even if corrupted")
	p = flag.Bool("p", false, "(encryption) use paranoid mode")
	r = flag.Bool("r", false, "(encryption) encode with Reed-Solomon")
	flag.Parse()

	if flag.NArg() == 0 {
		flag.Usage()
	}
	for _, v := range flag.Args() {
		if strings.HasPrefix(v, "-") {
			fmt.Println("Flags must be provided before arguments!")
			return 1
		}
	}

	if flag.NArg() == 1 {
		if strings.HasSuffix(flag.Arg(0), ".pcv") {
			mode = "decrypt"
		} else {
			mode = "encrypt"
		}
	} else {
		mode = "encrypt"
		for _, v := range flag.Args() {
			if strings.HasSuffix(v, ".pcv") {
				fmt.Println("Multiple items must not contain volumes.")
				return 1
			}
		}
	}

	return 0
}

var password []byte
var confirmp []byte
var err error

func auth() int {
	if mode == "encrypt" {
		fmt.Print("Password: ")
		password, err = term.ReadPassword(int(os.Stdin.Fd()))
		if err != nil {
			fmt.Println("Error reading password!")
			return 1
		}
		fmt.Print(strings.Repeat("*", len(password)), " | Confirm: ")
		confirmp, err = term.ReadPassword(int(os.Stdin.Fd()))
		if err != nil {
			fmt.Println("Error reading password!")
			return 1
		}
		fmt.Println(strings.Repeat("*", len(confirmp)))
		if !bytes.Equal(password, confirmp) {
			fmt.Println("Passwords don't match!")
			return 1
		}
	} else {
		fmt.Print("Password: ")
		password, err = term.ReadPassword(int(os.Stdin.Fd()))
		if err != nil {
			fmt.Println("Error reading password!")
			return 1
		}
		fmt.Println(strings.Repeat("*", len(password)))
	}

	return 0
}

var pin string
var pout string
var pzip string
var file *os.File
var writer *zip.Writer
var files []string
var interrupted bool

func prepare() int {
	if mode == "decrypt" {
		pin = flag.Arg(0)
		pout = strings.TrimSuffix(pin, ".pcv")
	} else {
		stat, err := os.Stat(flag.Arg(0))
		if flag.NArg() == 1 && err == nil && !stat.IsDir() {
			pin = flag.Arg(0)
			pout = pin + ".pcv"
		} else {
			items := []string{}
			for _, v := range flag.Args() {
				if strings.Contains(v, "../") || strings.HasPrefix(v, "/") {
					fmt.Println("Cannot encrypt outside of current directory.")
					return 1
				}
				matches, err := filepath.Glob(v)
				if err != nil {
					fmt.Println("Invalid glob pattern:", v)
					return 1
				}
				items = append(items, matches...)
			}
			for _, v := range items {
				stat, err := os.Stat(v)
				if err != nil {
					fmt.Println("Cannot access input:", v)
					return 1
				}
				if !stat.IsDir() {
					files = append(files, v)
				} else {
					filepath.Walk(v, func(path string, _ os.FileInfo, _ error) error {
						stat, err := os.Stat(path)
						if err == nil && !stat.IsDir() {
							files = append(files, path)
						}
						return nil
					})
				}
			}
			if len(files) == 0 {
				fmt.Println("Nothing to encrypt!")
				return 1
			}
		}
	}

	return 0
}

func compress() int {
	if files == nil {
		return 0
	}

	dir, err := os.Getwd()
	if err != nil {
		fmt.Println("Cannot get current working directory!")
		return 1
	}
	dir = filepath.ToSlash(dir)
	file, err = os.CreateTemp("", "picocrypt-cli-v2-*.zip.tmp")
	if err != nil {
		fmt.Println("Cannot create temporary file!")
		return 1
	}
	pzip = file.Name()
	writer = zip.NewWriter(file)

	fmt.Println("Info: creating temporary zip file " + pzip)

	for i, path := range files {
		stat, err := os.Stat(path)
		if err != nil {
			continue
		}
		header, err := zip.FileInfoHeader(stat)
		if err != nil {
			continue
		}
		abs, err := filepath.Abs(path)
		if err != nil {
			continue
		}
		abs = filepath.ToSlash(abs)
		header.Name = strings.TrimPrefix(abs, dir)
		header.Name = strings.TrimPrefix(header.Name, "/")
		header.Method = zip.Deflate
		entry, err := writer.CreateHeader(header)
		if err != nil {
			continue
		}
		fin, err := os.Open(path)
		if err != nil {
			writer.Close()
			file.Close()
			fmt.Println("Read access to input denied:", path)
			return 1
		}
		bar := progressbar.NewOptions(
			int(stat.Size()),
			progressbar.OptionClearOnFinish(),
			progressbar.OptionFullWidth(),
			progressbar.OptionShowBytes(true),
			progressbar.OptionUseIECUnits(true),
			progressbar.OptionSetDescription(
				fmt.Sprintf("Compressing [%d/%d]:", i+1, len(files)),
			),
		)
		_, err = io.Copy(io.MultiWriter(entry, bar), fin)
		fin.Close()
		if err != nil {
			if interrupted {
				time.Sleep(1 * time.Second)
			} else {
				writer.Close()
				file.Close()
				fmt.Println("Insufficient disk space!")
				return 1
			}
		}
		debug.FreeOSMemory()
	}
	writer.Close()
	file.Close()
	pin = file.Name()
	pout = "encrypted-" + strconv.Itoa(int(time.Now().Unix())) + ".zip.pcv"

	return 0
}

var fin *os.File
var fout *os.File
var padded bool
var salt []byte
var hkdfSalt []byte
var serpentIV []byte
var nonce []byte
var keyHash []byte
var keyHashRef []byte
var authTag []byte
var key []byte
var mac hash.Hash
var MiB = 1 << 20
var GiB = 1 << 30
var rs5, _ = infectious.NewFEC(5, 15)
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)

func rsEncode(rs *infectious.FEC, data []byte) []byte {
	res := make([]byte, rs.Total())
	rs.Encode(data, func(s infectious.Share) {
		res[s.Number] = s.Data[0]
	})
	return res
}

func rsDecode(rs *infectious.FEC, data []byte, fast bool) ([]byte, error) {
	if rs.Total() == 136 && fast {
		return data[:128], nil
	}
	tmp := make([]infectious.Share, rs.Total())
	for i := 0; i < rs.Total(); i++ {
		tmp[i].Number = i
		tmp[i].Data = append(tmp[i].Data, 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
}

func pad(data []byte) []byte {
	padLen := 128 - len(data)%128
	padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
	return append(data, padding...)
}

func unpad(data []byte) []byte {
	padLen := int(data[127])
	return data[:128-padLen]
}

func work() int {
	fin, err = os.Open(pin)
	if err != nil {
		fmt.Println("Error accessing input file:", pin)
		return 1
	}
	_, err = os.Stat(pout)
	if err == nil {
		fmt.Println("Output file already exists!")
		return 1
	}
	fout, err = os.Create(pout)
	if err != nil {
		fmt.Println("Error creating output file:", pout)
		return 1
	}
	stat, err := os.Stat(pin)
	if err != nil {
		fmt.Println("Error accessing input file:", pin)
		return 1
	}
	total := stat.Size()
	if mode == "decrypt" {
		total -= 789
	}

	if mode == "encrypt" {
		errs := make([]error, 10)
		salt = make([]byte, 16)
		hkdfSalt = make([]byte, 32)
		serpentIV = make([]byte, 16)
		nonce = make([]byte, 24)
		_, errs[0] = fout.Write(rsEncode(rs5, []byte("v1.48")))
		_, errs[1] = fout.Write(rsEncode(rs5, []byte("00000")))
		flags := make([]byte, 5)
		if *p {
			flags[0] = 1
		}
		if *r {
			flags[3] = 1
		}
		if total%int64(MiB) >= int64(MiB)-128 {
			flags[4] = 1
		}
		_, errs[2] = fout.Write(rsEncode(rs5, flags))
		if _, err := rand.Read(salt); err != nil {
			panic(err)
		}
		if _, err := rand.Read(hkdfSalt); err != nil {
			panic(err)
		}
		if _, err := rand.Read(serpentIV); err != nil {
			panic(err)
		}
		if _, err := rand.Read(nonce); err != nil {
			panic(err)
		}
		_, errs[3] = fout.Write(rsEncode(rs16, salt))
		_, errs[4] = fout.Write(rsEncode(rs32, hkdfSalt))
		_, errs[5] = fout.Write(rsEncode(rs16, serpentIV))
		_, errs[6] = fout.Write(rsEncode(rs24, nonce))
		_, errs[7] = fout.Write(make([]byte, 192))
		_, errs[8] = fout.Write(make([]byte, 96))
		_, errs[9] = fout.Write(make([]byte, 192))
		for _, err := range errs {
			if err != nil {
				fin.Close()
				fout.Close()
				fmt.Println("Insufficient disk space!")
				return 1
			}
		}
	} else {
		errs := make([]error, 9)
		version := make([]byte, 15)
		fin.Read(version)
		_, errs[0] = rsDecode(rs5, version, !(*f))
		tmp := make([]byte, 15)
		fin.Read(tmp)
		tmp, errs[1] = rsDecode(rs5, tmp, !(*f))
		comments, _ := strconv.Atoi(string(tmp))
		fin.Read(make([]byte, comments*3))
		total -= int64(comments) * 3
		flags := make([]byte, 15)
		fin.Read(flags)
		flags, errs[2] = rsDecode(rs5, flags, !(*f))
		*p = flags[0] == 1
		*r = flags[3] == 1
		padded = flags[4] == 1
		if flags[1] == 1 {
			fin.Close()
			fout.Close()
			fmt.Println("Keyfiles are not supported!")
			return 1
		}
		salt = make([]byte, 48)
		fin.Read(salt)
		salt, errs[3] = rsDecode(rs16, salt, !(*f))
		hkdfSalt = make([]byte, 96)
		fin.Read(hkdfSalt)
		hkdfSalt, errs[4] = rsDecode(rs32, hkdfSalt, !(*f))
		serpentIV = make([]byte, 48)
		fin.Read(serpentIV)
		serpentIV, errs[5] = rsDecode(rs16, serpentIV, !(*f))
		nonce = make([]byte, 72)
		fin.Read(nonce)
		nonce, errs[6] = rsDecode(rs24, nonce, !(*f))
		keyHashRef = make([]byte, 192)
		fin.Read(keyHashRef)
		keyHashRef, errs[7] = rsDecode(rs64, keyHashRef, !(*f))
		fin.Read(make([]byte, 96))
		authTag = make([]byte, 192)
		fin.Read(authTag)
		authTag, errs[8] = rsDecode(rs64, authTag, !(*f))
		for _, err := range errs {
			if err != nil {
				fin.Close()
				fout.Close()
				fmt.Println("The volume header is irrecoverably damaged!")
				return 1
			}
		}
	}

	if *p {
		key = argon2.IDKey(password, salt, 8, 1<<20, 8, 32)
	} else {
		key = argon2.IDKey(password, salt, 4, 1<<20, 4, 32)
	}
	debug.FreeOSMemory()
	tmp := sha3.New512()
	tmp.Write(key)
	keyHash = tmp.Sum(nil)
	if mode == "decrypt" {
		if !bytes.Equal(keyHash, keyHashRef) {
			fin.Close()
			fout.Close()
			fmt.Println("Incorrect password!")
			return 1
		}
	}

	done, counter := 0, 0
	chacha, _ := chacha20.NewUnauthenticatedCipher(key, nonce)
	subkey := make([]byte, 32)
	hkdf := hkdf.New(sha3.New256, key, hkdfSalt, nil)
	hkdf.Read(subkey)
	if *p {
		mac = hmac.New(sha3.New512, subkey)
	} else {
		mac, _ = blake2b.New512(subkey)
	}
	serpentKey := make([]byte, 32)
	hkdf.Read(serpentKey)
	s, _ := serpent.NewCipher(serpentKey)
	serpent := cipher.NewCTR(s, serpentIV)

	bar := progressbar.NewOptions(
		int(total),
		progressbar.OptionClearOnFinish(),
		progressbar.OptionFullWidth(),
		progressbar.OptionShowBytes(true),
		progressbar.OptionUseIECUnits(true),
		progressbar.OptionSetDescription(
			(func() string {
				if mode == "encrypt" {
					return "Encrypting:"
				}
				return "Decrypting:"
			})(),
		),
	)
	for {
		var src []byte
		if mode == "decrypt" && *r {
			src = make([]byte, MiB/128*136)
		} else {
			src = make([]byte, MiB)
		}
		size, err := fin.Read(src)
		if err != nil {
			break
		}
		src = src[:size]
		dst := make([]byte, len(src))
		bar.Write(src)

		if mode == "encrypt" {
			if *p {
				serpent.XORKeyStream(dst, src)
				copy(src, dst)
			}
			chacha.XORKeyStream(dst, src)
			mac.Write(dst)
			if *r {
				copy(src, dst)
				dst = nil
				if len(src) == MiB {
					for i := 0; i < MiB; i += 128 {
						dst = append(dst, rsEncode(rs128, src[i:i+128])...)
					}
				} else {
					chunks := math.Floor(float64(len(src)) / 128)
					for i := 0; float64(i) < chunks; i++ {
						dst = append(dst, rsEncode(rs128, src[i*128:(i+1)*128])...)
					}
					dst = append(dst, rsEncode(rs128, pad(src[int(chunks*128):]))...)
				}
			}
		} else {
			if *r {
				copy(dst, src)
				src = nil
				if len(dst) == MiB/128*136 {
					for i := 0; i < MiB/128*136; i += 136 {
						tmp, err := rsDecode(rs128, dst[i:i+136], !(*f))
						if err != nil {
							fin.Close()
							fout.Close()
							fmt.Println("\nThe input file is irrecoverably damaged.")
							return 1
						}
						if i == MiB/128*136-136 && done+MiB/128*136 >= int(total) && padded {
							tmp = unpad(tmp)
						}
						src = append(src, tmp...)
					}
				} else {
					chunks := len(dst)/136 - 1
					for i := 0; i < chunks; i++ {
						tmp, err := rsDecode(rs128, dst[i*136:(i+1)*136], !(*f))
						if err != nil {
							fin.Close()
							fout.Close()
							fmt.Println("\nThe input file is irrecoverably damaged.")
							return 1
						}
						src = append(src, tmp...)
					}
					tmp, err := rsDecode(rs128, dst[int(chunks)*136:], !(*f))
					if err != nil {
						fin.Close()
						fout.Close()
						fmt.Println("\nThe input file is irrecoverably damaged.")
						return 1
					}
					src = append(src, unpad(tmp)...)
				}
				dst = make([]byte, len(src))
			}
			mac.Write(src)
			chacha.XORKeyStream(dst, src)
			if *p {
				copy(src, dst)
				serpent.XORKeyStream(dst, src)
			}
		}

		_, err = fout.Write(dst)
		if err != nil {
			if interrupted {
				time.Sleep(1 * time.Second)
			} else {
				fin.Close()
				fout.Close()
				fmt.Println("\nInsufficient disk space!")
				return 1
			}
		}
		if mode == "decrypt" && *r {
			done += MiB / 128 * 136
		} else {
			done += MiB
		}

		if counter >= 60*GiB {
			nonce = make([]byte, 24)
			hkdf.Read(nonce)
			chacha, _ = chacha20.NewUnauthenticatedCipher(key, nonce)
			serpentIV = make([]byte, 16)
			hkdf.Read(serpentIV)
			serpent = cipher.NewCTR(s, serpentIV)
			counter = 0
		}
		debug.FreeOSMemory()
	}

	if mode == "encrypt" {
		fout.Seek(309, 0)
		fout.Write(rsEncode(rs64, keyHash))
		fout.Write(rsEncode(rs32, make([]byte, 32)))
		fout.Write(rsEncode(rs64, mac.Sum(nil)))
	} else {
		if !bytes.Equal(mac.Sum(nil), authTag) {
			fin.Close()
			fout.Close()
			if *k {
				fmt.Println("\nThe modified output has been kept.")
				return 0
			} else {
				fmt.Println("\nThe input volume is damaged or modified!")
				if *r {
					fmt.Println("Fortunately, this volume is encoded with Reed-Solomon.")
					fmt.Println("Try again using the '-f' flag to repair the corruption.")
				}
				return 1
			}
		}
	}

	fin.Close()
	fout.Close()
	fmt.Println("Completed ->", fout.Name())
	return 0
}

func main() {
	if parse() == 1 {
		os.Exit(1)
	}
	if auth() == 1 {
		os.Exit(1)
	}
	if prepare() == 1 {
		os.Exit(1)
	}

	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	go func() {
		<-c
		interrupted = true
		if fin != nil {
			fin.Close()
		}
		if fout != nil {
			fmt.Print("\nSystem interrupt detected, cleaning up incomplete output: ")
			fout.Close()
			if err := os.Remove(fout.Name()); err == nil {
				fmt.Print("Success.")
			} else {
				fmt.Print("Failure.")
			}
		}
		if pzip != "" {
			fmt.Print("\nSystem interrupt detected, cleaning up temporary files: ")
			writer.Close()
			file.Close()
			if err := os.Remove(pzip); err == nil {
				fmt.Print("Success.")
			} else {
				fmt.Print("Failure.")
			}
		}
		fmt.Println()
		os.Exit(1)
	}()

	if compress() == 1 {
		os.Remove(pzip)
		os.Exit(1)
	} else {
		defer os.Remove(pzip)
	}
	if work() == 1 {
		if pzip != "" {
			os.Remove(pzip)
		}
		if fout != nil {
			os.Remove(fout.Name())
		}
		os.Exit(1)
	}

	td := os.TempDir()
	matches, err := filepath.Glob(filepath.Join(td, "picocrypt-cli-v2-*.zip.tmp"))
	if err != nil {
		panic(err)
	}
	for _, v := range matches {
		os.Remove(v)
	}
}
