Picocrypt/src/Picocrypt.go
2025-01-27 00:13:11 -05:00

2483 lines
61 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
/*
Picocrypt v1.46
Copyright (c) Evan Su
Released under a GNU GPL v3 License
https://github.com/Picocrypt/Picocrypt
~ In cryptography we trust ~
*/
import (
"archive/zip"
"bytes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/subtle"
"errors"
"flag"
"fmt"
"hash"
"image"
"image/color"
"io"
"math"
"math/big"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/Picocrypt/dialog"
"github.com/Picocrypt/giu"
"github.com/Picocrypt/imgui-go"
"github.com/Picocrypt/infectious"
"github.com/Picocrypt/serpent"
"github.com/Picocrypt/zxcvbn-go"
"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"
)
// Constants
var KiB = 1 << 10
var MiB = 1 << 20
var GiB = 1 << 30
var TiB = 1 << 40
var WHITE = color.RGBA{0xff, 0xff, 0xff, 0xff}
var RED = color.RGBA{0xff, 0x00, 0x00, 0xff}
var GREEN = color.RGBA{0x00, 0xff, 0x00, 0xff}
var YELLOW = color.RGBA{0xff, 0xff, 0x00, 0xff}
var TRANSPARENT = color.RGBA{0x00, 0x00, 0x00, 0x00}
// Generic variables
var window *giu.MasterWindow
var version = "v1.46"
var dpi float32
var mode string
var working bool
var scanning bool
// Popup modals
var modalId int
var showPassgen bool
var showKeyfile bool
var showOverwrite bool
var showProgress bool
// Input and output files
var inputFile string
var inputFileOld string
var outputFile string
var onlyFiles []string
var onlyFolders []string
var allFiles []string
var inputLabel = "Drop files and folders into this window"
// Password and confirm password
var password string
var cpassword string
var passwordStrength int
var passwordState = giu.InputTextFlagsPassword
var passwordStateLabel = "Show"
// Password generator
var passgenLength int32 = 32
var passgenUpper bool
var passgenLower bool
var passgenNums bool
var passgenSymbols bool
var passgenCopy bool
// Keyfile variables
var keyfile bool
var keyfiles []string
var keyfileOrdered bool
var keyfileLabel = "None selected"
// Comments variables
var comments string
var commentsLabel = "Comments:"
var commentsDisabled bool
// Advanced options
var paranoid bool
var reedsolo bool
var deniability bool
var recursively bool
var split bool
var splitSize string
var splitUnits = []string{"KiB", "MiB", "GiB", "TiB", "Total"}
var splitSelected int32 = 1
var recombine bool
var compress bool
var delete bool
var autoUnzip bool
var sameLevel bool
var keep bool
var kept bool
// Status variables
var startLabel = "Start"
var mainStatus = "Ready"
var mainStatusColor = WHITE
var popupStatus string
// Progress variables
var progress float32
var progressInfo string
var speed float64
var eta string
var canCancel bool
// Reed-Solomon encoders
var rs1, _ = infectious.NewFEC(1, 3)
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)
var fastDecode bool
// Compression variables and passthrough
var compressDone int64
var compressTotal int64
var compressStart time.Time
type compressorProgress struct {
io.Reader
}
func (p *compressorProgress) Read(data []byte) (int, error) {
if !working {
return 0, io.EOF
}
read, err := p.Reader.Read(data)
compressDone += int64(read)
progress, speed, eta = statify(compressDone, compressTotal, compressStart)
if compress {
popupStatus = fmt.Sprintf("Compressing at %.2f MiB/s (ETA: %s)", speed, eta)
} else {
popupStatus = fmt.Sprintf("Combining at %.2f MiB/s (ETA: %s)", speed, eta)
}
giu.Update()
return read, err
}
// The main user interface
func draw() {
giu.SingleWindow().Flags(524351).Layout(
giu.Custom(func() {
if showPassgen {
giu.PopupModal("Generate password:##"+strconv.Itoa(modalId)).Flags(6).Layout(
giu.Row(
giu.Label("Length:"),
giu.SliderInt(&passgenLength, 4, 64).Size(giu.Auto),
),
giu.Checkbox("Uppercase", &passgenUpper),
giu.Checkbox("Lowercase", &passgenLower),
giu.Checkbox("Numbers", &passgenNums),
giu.Checkbox("Symbols", &passgenSymbols),
giu.Checkbox("Copy to clipboard", &passgenCopy),
giu.Row(
giu.Button("Cancel").Size(100, 0).OnClick(func() {
giu.CloseCurrentPopup()
showPassgen = false
}),
giu.Style().SetDisabled(!(passgenUpper || passgenLower || passgenNums || passgenSymbols)).To(
giu.Button("Generate").Size(100, 0).OnClick(func() {
password = genPassword()
cpassword = password
passwordStrength = zxcvbn.PasswordStrength(password, nil).Score
giu.CloseCurrentPopup()
showPassgen = false
}),
),
),
).Build()
giu.OpenPopup("Generate password:##" + strconv.Itoa(modalId))
giu.Update()
}
if showKeyfile {
giu.PopupModal("Manage keyfiles:##"+strconv.Itoa(modalId)).Flags(70).Layout(
giu.Label("Drag and drop your keyfiles here"),
giu.Custom(func() {
if mode != "decrypt" {
giu.Checkbox("Require correct order", &keyfileOrdered).Build()
giu.Tooltip("Ordering of keyfiles will matter").Build()
} else if keyfileOrdered {
giu.Label("Correct ordering is required").Build()
}
}),
giu.Custom(func() {
if len(keyfiles) > 0 {
giu.Separator().Build()
}
for _, i := range keyfiles {
giu.Label(filepath.Base(i)).Build()
}
}),
giu.Row(
giu.Button("Clear").Size(100, 0).OnClick(func() {
keyfiles = nil
if keyfile {
keyfileLabel = "Keyfiles required"
} else {
keyfileLabel = "None selected"
}
modalId++
giu.Update()
}),
giu.Tooltip("Remove all keyfiles"),
giu.Button("Done").Size(100, 0).OnClick(func() {
giu.CloseCurrentPopup()
showKeyfile = false
}),
),
).Build()
giu.OpenPopup("Manage keyfiles:##" + strconv.Itoa(modalId))
giu.Update()
}
if showOverwrite {
giu.PopupModal("Warning:##"+strconv.Itoa(modalId)).Flags(6).Layout(
giu.Label("Output already exists. Overwrite?"),
giu.Row(
giu.Button("No").Size(100, 0).OnClick(func() {
giu.CloseCurrentPopup()
showOverwrite = false
}),
giu.Button("Yes").Size(100, 0).OnClick(func() {
giu.CloseCurrentPopup()
showOverwrite = false
showProgress = true
fastDecode = true
canCancel = true
modalId++
giu.Update()
go func() {
work()
working = false
showProgress = false
giu.Update()
}()
}),
),
).Build()
giu.OpenPopup("Warning:##" + strconv.Itoa(modalId))
giu.Update()
}
if showProgress {
giu.PopupModal(" ##"+strconv.Itoa(modalId)).Flags(6).Layout(
giu.Row(
giu.ProgressBar(progress).Size(210, 0).Overlay(progressInfo),
giu.Style().SetDisabled(!canCancel).To(
giu.Button(func() string {
if working {
return "Cancel"
}
return "..."
}()).Size(58, 0).OnClick(func() {
working = false
canCancel = false
}),
),
),
giu.Label(popupStatus),
).Build()
giu.OpenPopup(" ##" + strconv.Itoa(modalId))
giu.Update()
}
}),
giu.Row(
giu.Label(inputLabel),
giu.Custom(func() {
bw, _ := giu.CalcTextSize("Clear")
p, _ := giu.GetWindowPadding()
bw += p * 2
giu.Dummy((bw+p)/-dpi, 0).Build()
giu.SameLine()
giu.Style().SetDisabled((len(allFiles) == 0 && len(onlyFiles) == 0) || scanning).To(
giu.Button("Clear").Size(bw/dpi, 0).OnClick(resetUI),
giu.Tooltip("Clear all input files and reset UI state"),
).Build()
}),
),
giu.Separator(),
giu.Style().SetDisabled((len(allFiles) == 0 && len(onlyFiles) == 0) || scanning).To(
giu.Label("Password:"),
giu.Row(
giu.Button(passwordStateLabel).Size(54, 0).OnClick(func() {
if passwordState == giu.InputTextFlagsPassword {
passwordState = giu.InputTextFlagsNone
passwordStateLabel = "Hide"
} else {
passwordState = giu.InputTextFlagsPassword
passwordStateLabel = "Show"
}
giu.Update()
}),
giu.Tooltip("Toggle the visibility of password entries"),
giu.Button("Clear").Size(54, 0).OnClick(func() {
password = ""
cpassword = ""
giu.Update()
}),
giu.Tooltip("Clear the password entries"),
giu.Button("Copy").Size(54, 0).OnClick(func() {
giu.Context.GetPlatform().SetClipboard(password)
giu.Update()
}),
giu.Tooltip("Copy the password into your clipboard"),
giu.Button("Paste").Size(54, 0).OnClick(func() {
tmp := giu.Context.GetPlatform().GetClipboard()
password = tmp
if mode != "decrypt" {
cpassword = tmp
}
passwordStrength = zxcvbn.PasswordStrength(password, nil).Score
giu.Update()
}),
giu.Tooltip("Paste a password from your clipboard"),
giu.Style().SetDisabled(mode == "decrypt").To(
giu.Button("Create").Size(54, 0).OnClick(func() {
showPassgen = true
modalId++
giu.Update()
}),
),
giu.Tooltip("Generate a cryptographically secure password"),
),
giu.Row(
giu.InputText(&password).Flags(passwordState).Size(302/dpi).OnChange(func() {
passwordStrength = zxcvbn.PasswordStrength(password, nil).Score
giu.Update()
}),
giu.Custom(func() {
c := giu.GetCanvas()
p := giu.GetCursorScreenPos()
col := color.RGBA{
uint8(0xc8 - 31*passwordStrength),
uint8(0x4c + 31*passwordStrength), 0x4b, 0xff,
}
if password == "" || mode == "decrypt" {
col = TRANSPARENT
}
path := p.Add(image.Pt(
int(math.Round(-20*float64(dpi))),
int(math.Round(12*float64(dpi))),
))
c.PathArcTo(path, 6*dpi, -math.Pi/2, math.Pi*(.4*float32(passwordStrength)-.1), -1)
c.PathStroke(col, false, 2)
}),
),
giu.Dummy(0, 0),
giu.Style().SetDisabled(password == "" || mode == "decrypt").To(
giu.Label("Confirm password:"),
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 = TRANSPARENT
}
path := p.Add(image.Pt(
int(math.Round(-20*float64(dpi))),
int(math.Round(12*float64(dpi))),
))
c.PathArcTo(path, 6*dpi, 0, 2*math.Pi, -1)
c.PathStroke(col, false, 2)
}),
),
),
giu.Dummy(0, 0),
giu.Style().SetDisabled(mode == "decrypt" && !keyfile && !deniability).To(
giu.Row(
giu.Label("Keyfiles:"),
giu.Button("Edit").Size(54, 0).OnClick(func() {
showKeyfile = true
modalId++
giu.Update()
}),
giu.Tooltip("Manage keyfiles to use for "+(func() string {
if mode != "decrypt" {
return "encryption"
}
return "decryption"
}())),
giu.Style().SetDisabled(mode == "decrypt").To(
giu.Button("Create").Size(54, 0).OnClick(func() {
f := dialog.File().Title("Choose where to save the keyfile")
f.SetStartDir(func() string {
if len(onlyFiles) > 0 {
return filepath.Dir(onlyFiles[0])
}
return filepath.Dir(onlyFolders[0])
}())
f.SetInitFilename("Keyfile")
file, err := f.Save()
if file == "" || err != nil {
return
}
fout, _ := os.Create(file)
data := make([]byte, 32)
if _, err := rand.Read(data); err != nil {
panic(err)
}
_, err = fout.Write(data)
fout.Close()
if err != nil {
insufficientSpace(nil, nil)
os.Remove(file)
} else {
mainStatus = "Ready"
mainStatusColor = WHITE
}
}),
giu.Tooltip("Generate a cryptographically secure keyfile"),
),
giu.Style().SetDisabled(true).To(
giu.InputText(&keyfileLabel).Size(giu.Auto),
),
),
),
),
giu.Separator(),
giu.Style().SetDisabled(mode != "decrypt" && ((len(keyfiles) == 0 && password == "") || (password != cpassword)) || deniability).To(
giu.Style().SetDisabled(mode == "decrypt" && (comments == "" || comments == "Comments are corrupted")).To(
giu.Label(commentsLabel),
giu.InputText(&comments).Size(giu.Auto).Flags(func() giu.InputTextFlags {
if commentsDisabled {
return giu.InputTextFlagsReadOnly
} else if deniability {
comments = ""
}
return giu.InputTextFlagsNone
}()),
giu.Custom(func() {
if !commentsDisabled {
giu.Tooltip("Note: comments are not encrypted!").Build()
}
}),
),
),
giu.Style().SetDisabled((len(keyfiles) == 0 && password == "") || (mode == "encrypt" && password != cpassword)).To(
giu.Label("Advanced:"),
giu.Custom(func() {
if mode != "decrypt" {
giu.Row(
giu.Checkbox("Paranoid mode", &paranoid),
giu.Tooltip("Provides the highest level of security attainable"),
giu.Dummy(-170, 0),
giu.Style().SetDisabled(recursively).To(
giu.Checkbox("Compress files", &compress).OnChange(func() {
if !(len(allFiles) > 1 || len(onlyFolders) > 0) {
if compress {
outputFile = filepath.Join(filepath.Dir(outputFile), "Encrypted") + ".zip.pcv"
} else {
outputFile = filepath.Join(filepath.Dir(outputFile), filepath.Base(inputFile)) + ".pcv"
}
}
}),
giu.Tooltip("Compress files with Deflate before encrypting"),
),
).Build()
giu.Row(
giu.Checkbox("Reed-Solomon", &reedsolo),
giu.Tooltip("Prevent file corruption with erasure coding"),
giu.Dummy(-170, 0),
giu.Checkbox("Delete files", &delete),
giu.Tooltip("Delete the input files after encryption"),
).Build()
giu.Row(
giu.Checkbox("Deniability", &deniability),
giu.Tooltip("Add plausible deniability to the volume\nIf enabled, comments will not be usable"),
giu.Dummy(-170, 0),
giu.Style().SetDisabled(!(len(allFiles) > 1 || len(onlyFolders) > 0)).To(
giu.Checkbox("Recursively", &recursively).OnChange(func() {
compress = false
}),
giu.Tooltip("Encrypt and decrypt recursive files individually"),
),
).Build()
giu.Row(
giu.Checkbox("Split into chunks:", &split),
giu.Tooltip("Split the output file into smaller chunks"),
giu.Dummy(-170, 0),
giu.InputText(&splitSize).Size(86/dpi).Flags(2).OnChange(func() {
split = splitSize != ""
}),
giu.Tooltip("Choose the chunk size"),
giu.Combo("##splitter", splitUnits[splitSelected], splitUnits, &splitSelected).Size(68),
giu.Tooltip("Choose the chunk units"),
).Build()
} else {
giu.Row(
giu.Style().SetDisabled(deniability).To(
giu.Checkbox("Force decrypt", &keep),
giu.Tooltip("Override security measures when decrypting"),
),
giu.Dummy(-170, 0),
giu.Checkbox("Delete volume", &delete),
giu.Tooltip("Delete the volume after a successful decryption"),
).Build()
giu.Row(
giu.Style().SetDisabled(!strings.HasSuffix(inputFile, ".zip.pcv")).To(
giu.Checkbox("Auto unzip", &autoUnzip).OnChange(func() {
if !autoUnzip {
sameLevel = false
}
}),
giu.Tooltip("Extract .zip upon decryption (may overwrite)"),
),
giu.Dummy(-170, 0),
giu.Style().SetDisabled(!autoUnzip).To(
giu.Checkbox("Same level", &sameLevel),
giu.Tooltip("Extract .zip contents to same folder as volume"),
),
).Build()
}
}),
giu.Style().SetDisabled(recursively).To(
giu.Label("Save output as:"),
giu.Custom(func() {
w, _ := giu.GetAvailableRegion()
bw, _ := giu.CalcTextSize("Change")
p, _ := giu.GetWindowPadding()
bw += p * 2
dw := w - bw - p
giu.Style().SetDisabled(true).To(
giu.InputText(func() *string {
tmp := ""
if outputFile == "" {
return &tmp
}
tmp = filepath.Base(outputFile)
if split {
tmp += ".*"
}
if recursively {
tmp = "(multiple values)"
}
return &tmp
}()).Size(dw / dpi / dpi).Flags(16384),
).Build()
giu.SameLine()
giu.Button("Change").Size(bw/dpi, 0).OnClick(func() {
f := dialog.File().Title("Choose where to save the output. Don't include extensions")
f.SetStartDir(func() string {
if len(onlyFiles) > 0 {
return filepath.Dir(onlyFiles[0])
}
return filepath.Dir(onlyFolders[0])
}())
// Prefill the filename
tmp := strings.TrimSuffix(filepath.Base(outputFile), ".pcv")
f.SetInitFilename(strings.TrimSuffix(tmp, filepath.Ext(tmp)))
if mode == "encrypt" && (len(allFiles) > 1 || len(onlyFolders) > 0 || compress) {
f.SetInitFilename("Encrypted")
}
// Get the chosen file path
file, err := f.Save()
if file == "" || err != nil {
return
}
file = filepath.Join(filepath.Dir(file), strings.Split(filepath.Base(file), ".")[0])
// Add the correct extensions
if mode == "encrypt" {
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
file += ".zip.pcv"
} else {
file += filepath.Ext(inputFile) + ".pcv"
}
} else {
if strings.HasSuffix(inputFile, ".zip.pcv") {
file += ".zip"
} else {
tmp := strings.TrimSuffix(filepath.Base(inputFile), ".pcv")
file += filepath.Ext(tmp)
}
}
outputFile = file
mainStatus = "Ready"
mainStatusColor = WHITE
}).Build()
giu.Tooltip("Save the output with a custom name and path").Build()
}),
),
giu.Dummy(0, 0),
giu.Separator(),
giu.Dummy(0, 0),
giu.Button(func() string {
if !recursively {
return startLabel
}
return "Process"
}()).Size(giu.Auto, 34).OnClick(func() {
if keyfile && keyfiles == nil {
mainStatus = "Please select your keyfiles"
mainStatusColor = RED
return
}
tmp, err := strconv.Atoi(splitSize)
if split && (splitSize == "" || tmp <= 0 || err != nil) {
mainStatus = "Invalid chunk size"
mainStatusColor = RED
return
}
// Check if output file already exists
_, err = os.Stat(outputFile)
// Check if any split chunks already exist
if split {
names, _ := filepath.Glob(outputFile + ".*")
if len(names) > 0 {
err = nil
} else {
err = os.ErrNotExist
}
}
// If files already exist, show the overwrite modal
if err == nil && !recursively {
showOverwrite = true
modalId++
giu.Update()
} else { // Nothing to worry about, start working
showProgress = true
fastDecode = true
canCancel = true
modalId++
giu.Update()
if !recursively {
go func() {
work()
working = false
showProgress = false
giu.Update()
}()
} else {
// Store variables as they will be cleared
oldPassword := password
oldKeyfile := keyfile
oldKeyfiles := keyfiles
oldKeyfileOrdered := keyfileOrdered
oldKeyfileLabel := keyfileLabel
oldComments := comments
oldParanoid := paranoid
oldReedsolo := reedsolo
oldDeniability := deniability
oldSplit := split
oldSplitSize := splitSize
oldSplitSelected := splitSelected
oldDelete := delete
files := allFiles
go func() {
for _, file := range files {
// Simulate dropping the file
onDrop([]string{file})
// Restore variables and options
password = oldPassword
cpassword = oldPassword
keyfile = oldKeyfile
keyfiles = oldKeyfiles
keyfileOrdered = oldKeyfileOrdered
keyfileLabel = oldKeyfileLabel
comments = oldComments
paranoid = oldParanoid
reedsolo = oldReedsolo
deniability = oldDeniability
split = oldSplit
splitSize = oldSplitSize
splitSelected = oldSplitSelected
delete = oldDelete
work()
if !working {
resetUI()
cancel(nil, nil)
showProgress = false
giu.Update()
return
}
}
working = false
showProgress = false
giu.Update()
}()
}
}
}),
giu.Style().SetColor(giu.StyleColorText, mainStatusColor).To(
giu.Label(mainStatus),
),
),
giu.Custom(func() {
window.SetSize(int(318*dpi), giu.GetCursorPos().Y+1)
}),
)
}
func onDrop(names []string) {
if showKeyfile {
keyfiles = append(keyfiles, names...)
// Make sure keyfiles are accessible, remove duplicates
var tmp []string
for _, i := range keyfiles {
duplicate := false
for _, j := range tmp {
if i == j {
duplicate = true
}
}
stat, _ := os.Stat(i)
fin, err := os.Open(i)
if err == nil {
fin.Close()
} else {
showKeyfile = false
resetUI()
accessDenied("Keyfile read")
giu.Update()
return
}
if !duplicate && !stat.IsDir() {
tmp = append(tmp, i)
}
}
keyfiles = tmp
// Update the keyfile status
if len(keyfiles) == 0 {
keyfileLabel = "None selected"
} else if len(keyfiles) == 1 {
keyfileLabel = "Using 1 keyfile"
} else {
keyfileLabel = fmt.Sprintf("Using %d keyfiles", len(keyfiles))
}
modalId++
giu.Update()
return
}
scanning = true
files, folders := 0, 0
compressDone, compressTotal = 0, 0
resetUI()
// One item dropped
if len(names) == 1 {
stat, _ := os.Stat(names[0])
// A folder was dropped
if stat.IsDir() {
folders++
mode = "encrypt"
inputLabel = "1 folder"
startLabel = "Encrypt"
onlyFolders = append(onlyFolders, names[0])
inputFile = filepath.Join(filepath.Dir(names[0]), "Encrypted") + ".zip"
outputFile = inputFile + ".pcv"
} else { // A file was dropped
files++
// Is the file a part of a split volume?
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 {
mode = "decrypt"
inputLabel = "Volume for decryption"
startLabel = "Decrypt"
commentsLabel = "Comments (read-only):"
commentsDisabled = true
// Get the correct input and output filenames
if isSplit {
ind := strings.Index(names[0], ".pcv")
names[0] = names[0][:ind+4]
inputFile = names[0]
outputFile = names[0][:ind]
recombine = true
// Find out the number of splitted chunks
totalFiles := 0
for {
stat, err := os.Stat(fmt.Sprintf("%s.%d", inputFile, totalFiles))
if err != nil {
break
}
totalFiles++
compressTotal += stat.Size()
}
} else {
outputFile = names[0][:len(names[0])-4]
}
// Open the input file in read-only mode
var fin *os.File
var err error
if isSplit {
fin, err = os.Open(names[0] + ".0")
} else {
fin, err = os.Open(names[0])
}
if err != nil {
resetUI()
accessDenied("Read")
return
}
// Check if version can be read from header
tmp := make([]byte, 15)
fin.Read(tmp)
tmp, err = rsDecode(rs5, tmp)
if valid, _ := regexp.Match(`^v\d\.\d{2}`, tmp); !valid || err != nil {
// Volume has plausible deniability
deniability = true
mainStatus = "Can't read header, assuming volume is deniable"
fin.Close()
} else {
// Read comments from file and check for corruption
tmp = make([]byte, 15)
fin.Read(tmp)
tmp, err = rsDecode(rs5, tmp)
if err == nil {
commentsLength, _ := strconv.Atoi(string(tmp))
tmp = make([]byte, commentsLength*3)
fin.Read(tmp)
comments = ""
for i := 0; i < commentsLength*3; i += 3 {
t, err := rsDecode(rs1, tmp[i:i+3])
if err != nil {
comments = "Comments are corrupted"
break
}
comments += string(t)
}
} else {
comments = "Comments are corrupted"
}
// Read flags from file and check for corruption
flags := make([]byte, 15)
fin.Read(flags)
fin.Close()
flags, err = rsDecode(rs5, flags)
if err != nil {
mainStatus = "The volume header is damaged"
mainStatusColor = RED
return
}
// Update UI and variables according to flags
if flags[1] == 1 {
keyfile = true
keyfileLabel = "Keyfiles required"
} else {
keyfileLabel = "Not applicable"
}
if flags[2] == 1 {
keyfileOrdered = true
}
}
} else { // One file was dropped for encryption
mode = "encrypt"
inputLabel = "1 file"
startLabel = "Encrypt"
inputFile = names[0]
outputFile = names[0] + ".pcv"
}
// Add the file
onlyFiles = append(onlyFiles, names[0])
inputFile = names[0]
if !isSplit {
compressTotal += stat.Size()
}
}
} else { // There are multiple dropped items
mode = "encrypt"
startLabel = "Encrypt"
// Go through each dropped item and add to corresponding slices
for _, name := range names {
stat, _ := os.Stat(name)
if stat.IsDir() {
folders++
onlyFolders = append(onlyFolders, name)
} else {
files++
onlyFiles = append(onlyFiles, name)
allFiles = append(allFiles, name)
compressTotal += stat.Size()
inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal))
giu.Update()
}
}
// Update UI with the number of files and folders selected
if folders == 0 {
inputLabel = fmt.Sprintf("%d files", files)
} else if files == 0 {
inputLabel = fmt.Sprintf("%d folders", folders)
} else {
if files == 1 && folders > 1 {
inputLabel = fmt.Sprintf("1 file and %d folders", folders)
} else if folders == 1 && files > 1 {
inputLabel = fmt.Sprintf("%d files and 1 folder", files)
} else if folders == 1 && files == 1 {
inputLabel = "1 file and 1 folder"
} else {
inputLabel = fmt.Sprintf("%d files and %d folders", files, folders)
}
}
// Set the input and output paths
inputFile = filepath.Join(filepath.Dir(names[0]), "Encrypted") + ".zip"
outputFile = inputFile + ".pcv"
}
// Recursively add all files in 'onlyFolders' to 'allFiles'
go func() {
oldInputLabel := inputLabel
for _, name := range onlyFolders {
filepath.Walk(name, func(path string, _ os.FileInfo, _ error) error {
stat, err := os.Stat(path)
// If 'path' is a valid file path, add to 'allFiles'
if err == nil && !stat.IsDir() {
allFiles = append(allFiles, path)
compressTotal += stat.Size()
inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal))
giu.Update()
}
return nil
})
}
inputLabel = fmt.Sprintf("%s (%s)", oldInputLabel, sizeify(compressTotal))
scanning = false
giu.Update()
}()
}
func work() {
popupStatus = "Starting..."
mainStatus = "Working..."
mainStatusColor = WHITE
working = true
padded := false
giu.Update()
// Cryptography values
var salt []byte // Argon2 salt, 16 bytes
var hkdfSalt []byte // HKDF-SHA3 salt, 32 bytes
var serpentIV []byte // Serpent IV, 16 bytes
var nonce []byte // 24-byte XChaCha20 nonce
var keyHash []byte // SHA3-512 hash of encryption key
var keyHashRef []byte // Same as 'keyHash', but used for comparison
var keyfileKey []byte // The SHA3-256 hashes of keyfiles
var keyfileHash = make([]byte, 32) // The SHA3-256 of 'keyfileKey'
var keyfileHashRef []byte // Same as 'keyfileHash', but used for comparison
var authTag []byte // 64-byte authentication tag (BLAKE2b or HMAC-SHA3)
// Combine/compress all files into a .zip file if needed
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
// Consider case where compressing only one file
files := allFiles
if len(allFiles) == 0 {
files = onlyFiles
}
// Get the root directory of the selected files
var rootDir string
if len(onlyFolders) > 0 {
rootDir = filepath.Dir(onlyFolders[0])
} else {
rootDir = filepath.Dir(onlyFiles[0])
}
// Open a temporary .zip for writing
inputFile = strings.TrimSuffix(outputFile, ".pcv")
file, err := os.Create(inputFile)
if err != nil { // Make sure file is writable
accessDenied("Write")
return
}
// Add each file to the .zip
writer := zip.NewWriter(file)
compressStart = time.Now()
for i, path := range files {
progressInfo = fmt.Sprintf("%d/%d", i+1, len(files))
giu.Update()
// Create file info header (size, last modified, etc.)
stat, err := os.Stat(path)
if err != nil {
continue // Skip temporary and inaccessible files
}
header, _ := zip.FileInfoHeader(stat)
header.Name = strings.TrimPrefix(path, rootDir)
header.Name = filepath.ToSlash(header.Name)
header.Name = strings.TrimPrefix(header.Name, "/")
if compress {
header.Method = zip.Deflate
} else {
header.Method = zip.Store
}
// Open the file for reading
entry, _ := writer.CreateHeader(header)
fin, err := os.Open(path)
if err != nil {
writer.Close()
file.Close()
os.Remove(inputFile)
resetUI()
accessDenied("Read")
return
}
// Use a passthrough to catch compression progress
passthrough := &compressorProgress{Reader: fin}
buf := make([]byte, MiB)
_, err = io.CopyBuffer(entry, passthrough, buf)
fin.Close()
if err != nil {
writer.Close()
insufficientSpace(nil, file)
os.Remove(inputFile)
return
}
if !working {
writer.Close()
cancel(nil, file)
os.Remove(inputFile)
return
}
}
writer.Close()
file.Close()
}
// Recombine a split file if necessary
if recombine {
totalFiles := 0
totalBytes := int64(0)
done := 0
// Find out the number of splitted chunks
for {
stat, err := os.Stat(fmt.Sprintf("%s.%d", inputFile, totalFiles))
if err != nil {
break
}
totalFiles++
totalBytes += stat.Size()
}
// Make sure not to overwrite anything
_, err := os.Stat(outputFile + ".pcv")
if err == nil { // File already exists
mainStatus = "Please remove " + filepath.Base(outputFile+".pcv")
mainStatusColor = RED
return
}
// Create a .pcv to combine chunks into
fout, err := os.Create(outputFile + ".pcv")
if err != nil { // Make sure file is writable
accessDenied("Write")
return
}
// Merge all chunks into one file
startTime := time.Now()
for i := 0; i < totalFiles; i++ {
fin, err := os.Open(fmt.Sprintf("%s.%d", inputFile, i))
if err != nil {
fout.Close()
os.Remove(outputFile + ".pcv")
resetUI()
accessDenied("Read")
return
}
for {
if !working {
cancel(fin, fout)
os.Remove(outputFile + ".pcv")
return
}
// Copy from the chunk into the .pcv
data := make([]byte, MiB)
read, err := fin.Read(data)
if err != nil {
break
}
data = data[:read]
_, err = fout.Write(data)
done += read
if err != nil {
insufficientSpace(fin, fout)
os.Remove(outputFile + ".pcv")
return
}
// Update the stats
progress, speed, eta = statify(int64(done), totalBytes, startTime)
progressInfo = fmt.Sprintf("%d/%d", i+1, totalFiles)
popupStatus = fmt.Sprintf("Recombining at %.2f MiB/s (ETA: %s)", speed, eta)
giu.Update()
}
fin.Close()
}
fout.Close()
inputFileOld = inputFile
inputFile = outputFile + ".pcv"
}
// Input volume has plausible deniability
if mode == "decrypt" && deniability {
popupStatus = "Removing deniability protection..."
progressInfo = ""
progress = 0
canCancel = false
giu.Update()
// Get size of volume for showing progress
stat, _ := os.Stat(inputFile)
total := stat.Size()
// Rename input volume to free up the filename
fin, _ := os.Open(inputFile)
for strings.HasSuffix(inputFile, ".tmp") {
inputFile = strings.TrimSuffix(inputFile, ".tmp")
}
inputFile += ".tmp"
fout, _ := os.Create(inputFile)
// Get the Argon2 salt and XChaCha20 nonce from input volume
salt := make([]byte, 16)
nonce := make([]byte, 24)
fin.Read(salt)
fin.Read(nonce)
// Generate key and XChaCha20
key := argon2.IDKey([]byte(password), salt, 4, 1<<20, 4, 32)
chacha, _ := chacha20.NewUnauthenticatedCipher(key, nonce)
// Decrypt the entire volume
done, counter := 0, 0
for {
src := make([]byte, MiB)
size, err := fin.Read(src)
if err != nil {
break
}
src = src[:size]
dst := make([]byte, len(src))
chacha.XORKeyStream(dst, src)
fout.Write(dst)
// Update stats
done += size
counter += MiB
progress = float32(float64(done) / float64(total))
giu.Update()
// Change nonce after 60 GiB to prevent overflow
if counter >= 60*GiB {
tmp := sha3.New256()
tmp.Write(nonce)
nonce = tmp.Sum(nil)[:24]
chacha, _ = chacha20.NewUnauthenticatedCipher(key, nonce)
counter = 0
}
}
fin.Close()
fout.Close()
// Check if the version can be read from the volume
fin, _ = os.Open(inputFile)
tmp := make([]byte, 15)
fin.Read(tmp)
fin.Close()
tmp, err := rsDecode(rs5, tmp)
if valid, _ := regexp.Match(`^v1\.\d{2}`, tmp); !valid || err != nil {
os.Remove(inputFile)
inputFile = strings.TrimSuffix(inputFile, ".tmp")
broken(nil, nil, "Password is incorrect or the file is not a volume", true)
if recombine {
inputFile = inputFileOld
}
return
}
}
canCancel = false
progress = 0
progressInfo = ""
giu.Update()
// Subtract the header size from the total size if decrypting
stat, _ := os.Stat(inputFile)
total := stat.Size()
if mode == "decrypt" {
total -= 789
}
// Open input file in read-only mode
fin, err := os.Open(inputFile)
if err != nil {
resetUI()
accessDenied("Read")
return
}
// Setup output file
var fout *os.File
// If encrypting, generate values and write to file
if mode == "encrypt" {
popupStatus = "Generating values..."
giu.Update()
// Stores any errors when writing to file
errs := make([]error, 11)
// Make sure not to overwrite anything
_, err = os.Stat(outputFile)
if split && err == nil { // File already exists
fin.Close()
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
mainStatus = "Please remove " + filepath.Base(outputFile)
mainStatusColor = RED
return
}
// Create the output file
fout, err = os.Create(outputFile)
if err != nil {
fin.Close()
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
accessDenied("Write")
return
}
// Set up cryptographic values
salt = make([]byte, 16)
hkdfSalt = make([]byte, 32)
serpentIV = make([]byte, 16)
nonce = make([]byte, 24)
// Write the program version to file
_, errs[0] = fout.Write(rsEncode(rs5, []byte(version)))
if len(comments) > 99999 {
panic(errors.New("comments exceed maximum length"))
}
// Encode and write the comment length to file
commentsLength := []byte(fmt.Sprintf("%05d", len(comments)))
_, errs[1] = fout.Write(rsEncode(rs5, commentsLength))
// Encode the comment and write to file
for _, i := range []byte(comments) {
_, err := fout.Write(rsEncode(rs1, []byte{i}))
if err != nil {
errs[2] = err
}
}
// Configure flags and write to file
flags := make([]byte, 5)
if paranoid { // Paranoid mode selected
flags[0] = 1
}
if len(keyfiles) > 0 { // Keyfiles are being used
flags[1] = 1
}
if keyfileOrdered { // Order of keyfiles matter
flags[2] = 1
}
if reedsolo { // Full Reed-Solomon encoding is selected
flags[3] = 1
}
if total%int64(MiB) >= int64(MiB)-128 { // Reed-Solomon internals
flags[4] = 1
}
_, errs[3] = fout.Write(rsEncode(rs5, flags))
// Fill values with Go's CSPRNG
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)
}
// Encode values with Reed-Solomon and write to file
_, errs[4] = fout.Write(rsEncode(rs16, salt))
_, errs[5] = fout.Write(rsEncode(rs32, hkdfSalt))
_, errs[6] = fout.Write(rsEncode(rs16, serpentIV))
_, errs[7] = fout.Write(rsEncode(rs24, nonce))
// Write placeholders for future use
_, errs[8] = fout.Write(make([]byte, 192)) // Hash of encryption key
_, errs[9] = fout.Write(make([]byte, 96)) // Hash of keyfile key
_, errs[10] = fout.Write(make([]byte, 192)) // BLAKE2b/HMAC-SHA3 tag
for _, err := range errs {
if err != nil {
insufficientSpace(fin, fout)
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
os.Remove(outputFile)
return
}
}
} else { // Decrypting, read values from file and decode
popupStatus = "Reading values..."
giu.Update()
// Stores any Reed-Solomon decoding errors
errs := make([]error, 10)
version := make([]byte, 15)
fin.Read(version)
_, errs[0] = rsDecode(rs5, version)
tmp := make([]byte, 15)
fin.Read(tmp)
tmp, errs[1] = rsDecode(rs5, tmp)
if valid, err := regexp.Match(`^\d{5}$`, tmp); !valid || err != nil {
broken(fin, nil, "Unable to read comments length", true)
return
}
commentsLength, _ := strconv.Atoi(string(tmp))
fin.Read(make([]byte, commentsLength*3))
total -= int64(commentsLength) * 3
flags := make([]byte, 15)
fin.Read(flags)
flags, errs[2] = rsDecode(rs5, flags)
paranoid = flags[0] == 1
reedsolo = flags[3] == 1
padded = flags[4] == 1
if deniability {
keyfile = flags[1] == 1
keyfileOrdered = flags[2] == 1
}
salt = make([]byte, 48)
fin.Read(salt)
salt, errs[3] = rsDecode(rs16, salt)
hkdfSalt = make([]byte, 96)
fin.Read(hkdfSalt)
hkdfSalt, errs[4] = rsDecode(rs32, hkdfSalt)
serpentIV = make([]byte, 48)
fin.Read(serpentIV)
serpentIV, errs[5] = rsDecode(rs16, serpentIV)
nonce = make([]byte, 72)
fin.Read(nonce)
nonce, errs[6] = rsDecode(rs24, nonce)
keyHashRef = make([]byte, 192)
fin.Read(keyHashRef)
keyHashRef, errs[7] = rsDecode(rs64, keyHashRef)
keyfileHashRef = make([]byte, 96)
fin.Read(keyfileHashRef)
keyfileHashRef, errs[8] = rsDecode(rs32, keyfileHashRef)
authTag = make([]byte, 192)
fin.Read(authTag)
authTag, errs[9] = rsDecode(rs64, authTag)
// If there was an issue during decoding, the header is corrupted
for _, err := range errs {
if err != nil {
if keep { // If the user chooses to force decrypt
kept = true
} else {
broken(fin, nil, "The volume header is damaged", true)
return
}
}
}
}
popupStatus = "Deriving key..."
giu.Update()
// Derive encryption keys and subkeys
var key []byte
if paranoid {
key = argon2.IDKey(
[]byte(password),
salt,
8, // 8 passes
1<<20, // 1 GiB memory
8, // 8 threads
32, // 32-byte output key
)
} else {
key = argon2.IDKey(
[]byte(password),
salt,
4,
1<<20,
4,
32,
)
}
// If keyfiles are being used
if len(keyfiles) > 0 || keyfile {
popupStatus = "Reading keyfiles..."
giu.Update()
var keyfileTotal int64
for _, path := range keyfiles {
stat, _ := os.Stat(path)
keyfileTotal += stat.Size()
}
if keyfileOrdered { // If order matters, hash progressively
var tmp = sha3.New256()
var keyfileDone int
// For each keyfile...
for _, path := range keyfiles {
fin, _ := os.Open(path)
for { // Read in chunks of 1 MiB
data := make([]byte, MiB)
size, err := fin.Read(data)
if err != nil {
break
}
data = data[:size]
tmp.Write(data) // Hash the data
// Update progress
keyfileDone += size
progress = float32(keyfileDone) / float32(keyfileTotal)
giu.Update()
}
fin.Close()
}
keyfileKey = tmp.Sum(nil) // Get the SHA3-256
// Store a hash of 'keyfileKey' for comparison
tmp = sha3.New256()
tmp.Write(keyfileKey)
keyfileHash = tmp.Sum(nil)
} else { // If order doesn't matter, hash individually and combine
var keyfileDone int
// For each keyfile...
for _, path := range keyfiles {
fin, _ := os.Open(path)
tmp := sha3.New256()
for { // Read in chunks of 1 MiB
data := make([]byte, MiB)
size, err := fin.Read(data)
if err != nil {
break
}
data = data[:size]
tmp.Write(data) // Hash the data
// Update progress
keyfileDone += size
progress = float32(keyfileDone) / float32(keyfileTotal)
giu.Update()
}
fin.Close()
sum := tmp.Sum(nil) // Get the SHA3-256
// XOR keyfile hash with 'keyfileKey'
if keyfileKey == nil {
keyfileKey = sum
} else {
for i, j := range sum {
keyfileKey[i] ^= j
}
}
}
// Store a hash of 'keyfileKey' for comparison
tmp := sha3.New256()
tmp.Write(keyfileKey)
keyfileHash = tmp.Sum(nil)
}
}
popupStatus = "Calculating values..."
giu.Update()
// Hash the encryption key for comparison when decrypting
tmp := sha3.New512()
tmp.Write(key)
keyHash = tmp.Sum(nil)
// Validate the password and/or keyfiles
if mode == "decrypt" {
keyCorrect := subtle.ConstantTimeCompare(keyHash, keyHashRef) == 1
keyfileCorrect := subtle.ConstantTimeCompare(keyfileHash, keyfileHashRef) == 1
incorrect := !keyCorrect
if keyfile || len(keyfiles) > 0 {
incorrect = !keyCorrect || !keyfileCorrect
}
// If something is incorrect
if incorrect {
if keep {
kept = true
} else {
if !keyCorrect {
mainStatus = "The provided password is incorrect"
} else {
if keyfileOrdered {
mainStatus = "Incorrect keyfiles or ordering"
} else {
mainStatus = "Incorrect keyfiles"
}
if deniability {
fin.Close()
os.Remove(inputFile)
inputFile = strings.TrimSuffix(inputFile, ".tmp")
}
}
broken(fin, nil, mainStatus, true)
if recombine {
inputFile = inputFileOld
}
return
}
}
// Create the output file for decryption
fout, err = os.Create(outputFile)
if err != nil {
fin.Close()
if recombine {
os.Remove(inputFile)
}
accessDenied("Write")
return
}
}
if len(keyfiles) > 0 || keyfile {
// Prevent an even number of duplicate keyfiles
if bytes.Equal(keyfileKey, make([]byte, 32)) {
mainStatus = "Duplicate keyfiles detected"
mainStatusColor = RED
fin.Close()
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
fout.Close()
os.Remove(fout.Name())
return
}
// XOR the encryption key with the keyfile key
tmp := key
key = make([]byte, 32)
for i := range key {
key[i] = tmp[i] ^ keyfileKey[i]
}
}
done, counter := 0, 0
chacha, _ := chacha20.NewUnauthenticatedCipher(key, nonce)
// Use HKDF-SHA3 to generate a subkey for the MAC
var mac hash.Hash
subkey := make([]byte, 32)
hkdf := hkdf.New(sha3.New256, key, hkdfSalt, nil)
hkdf.Read(subkey)
if paranoid {
mac = hmac.New(sha3.New512, subkey) // HMAC-SHA3
} else {
mac, _ = blake2b.New512(subkey) // Keyed BLAKE2b
}
// Generate another subkey for use as Serpent's key
serpentKey := make([]byte, 32)
hkdf.Read(serpentKey)
s, _ := serpent.NewCipher(serpentKey)
serpent := cipher.NewCTR(s, serpentIV)
// Start the main encryption process
canCancel = true
startTime := time.Now()
for {
if !working {
cancel(fin, fout)
if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
os.Remove(outputFile)
return
}
// Read in data from the file
var src []byte
if mode == "decrypt" && reedsolo {
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))
// Do the actual encryption
if mode == "encrypt" {
if paranoid {
serpent.XORKeyStream(dst, src)
copy(src, dst)
}
chacha.XORKeyStream(dst, src)
mac.Write(dst)
if reedsolo {
copy(src, dst)
dst = nil
// If a full MiB is available
if len(src) == MiB {
// Encode every chunk
for i := 0; i < MiB; i += 128 {
dst = append(dst, rsEncode(rs128, src[i:i+128])...)
}
} else {
// Encode the full chunks
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])...)
}
// Pad and encode the final partial chunk
dst = append(dst, rsEncode(rs128, pad(src[int(chunks*128):]))...)
}
}
} else { // Decryption
if reedsolo {
copy(dst, src)
src = nil
// If a complete 1 MiB block is available
if len(dst) == MiB/128*136 {
// Decode every chunk
for i := 0; i < MiB/128*136; i += 136 {
tmp, err := rsDecode(rs128, dst[i:i+136])
if err != nil {
if keep {
kept = true
} else {
broken(fin, fout, "The input file is irrecoverably damaged", false)
return
}
}
if i == MiB/128*136-136 && done+MiB/128*136 >= int(total) && padded {
tmp = unpad(tmp)
}
src = append(src, tmp...)
if !fastDecode && i%17408 == 0 {
progress, speed, eta = statify(int64(done+i), total, startTime)
progressInfo = fmt.Sprintf("%.2f%%", progress*100)
popupStatus = fmt.Sprintf("Repairing at %.2f MiB/s (ETA: %s)", speed, eta)
giu.Update()
}
}
} else {
// Decode the full chunks
chunks := len(dst)/136 - 1
for i := 0; i < chunks; i++ {
tmp, err := rsDecode(rs128, dst[i*136:(i+1)*136])
if err != nil {
if keep {
kept = true
} else {
broken(fin, fout, "The input file is irrecoverably damaged", false)
return
}
}
src = append(src, tmp...)
if !fastDecode && i%128 == 0 {
progress, speed, eta = statify(int64(done+i*136), total, startTime)
progressInfo = fmt.Sprintf("%.2f%%", progress*100)
popupStatus = fmt.Sprintf("Repairing at %.2f MiB/s (ETA: %s)", speed, eta)
giu.Update()
}
}
// Unpad and decode the final partial chunk
tmp, err := rsDecode(rs128, dst[int(chunks)*136:])
if err != nil {
if keep {
kept = true
} else {
broken(fin, fout, "The input file is irrecoverably damaged", false)
return
}
}
src = append(src, unpad(tmp)...)
}
dst = make([]byte, len(src))
}
mac.Write(src)
chacha.XORKeyStream(dst, src)
if paranoid {
copy(src, dst)
serpent.XORKeyStream(dst, src)
}
}
// Write the data to output file
_, err = fout.Write(dst)
if err != nil {
insufficientSpace(fin, fout)
if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
os.Remove(outputFile)
return
}
// Update stats
if mode == "decrypt" && reedsolo {
done += MiB / 128 * 136
} else {
done += MiB
}
counter += MiB
progress, speed, eta = statify(int64(done), total, startTime)
progressInfo = fmt.Sprintf("%.2f%%", progress*100)
if mode == "encrypt" {
popupStatus = fmt.Sprintf("Encrypting at %.2f MiB/s (ETA: %s)", speed, eta)
} else {
if fastDecode {
popupStatus = fmt.Sprintf("Decrypting at %.2f MiB/s (ETA: %s)", speed, eta)
}
}
giu.Update()
// Change nonce/IV after 60 GiB to prevent overflow
if counter >= 60*GiB {
// ChaCha20
nonce = make([]byte, 24)
hkdf.Read(nonce)
chacha, _ = chacha20.NewUnauthenticatedCipher(key, nonce)
// Serpent
serpentIV = make([]byte, 16)
hkdf.Read(serpentIV)
serpent = cipher.NewCTR(s, serpentIV)
// Reset counter to 0
counter = 0
}
}
progress = 0
progressInfo = ""
giu.Update()
if mode == "encrypt" {
popupStatus = "Writing values..."
giu.Update()
// Seek back to header and write important values
fout.Seek(int64(309+len(comments)*3), 0)
fout.Write(rsEncode(rs64, keyHash))
fout.Write(rsEncode(rs32, keyfileHash))
fout.Write(rsEncode(rs64, mac.Sum(nil)))
} else {
popupStatus = "Comparing values..."
giu.Update()
// Validate the authenticity of decrypted data
if subtle.ConstantTimeCompare(mac.Sum(nil), authTag) == 0 {
// Decrypt again but this time rebuilding the input data
if reedsolo && fastDecode {
fastDecode = false
fin.Close()
fout.Close()
work()
return
}
if keep {
kept = true
} else {
broken(fin, fout, "The input file is damaged or modified", false)
return
}
}
}
fin.Close()
fout.Close()
// Add plausible deniability
if mode == "encrypt" && deniability {
popupStatus = "Adding plausible deniability..."
canCancel = false
giu.Update()
// Get size of volume for showing progress
stat, _ := os.Stat(fout.Name())
total := stat.Size()
// Rename the output volume to free up the filename
os.Rename(fout.Name(), fout.Name()+".tmp")
fin, _ := os.Open(fout.Name() + ".tmp")
fout, _ := os.Create(fout.Name())
// Use a random Argon2 salt and XChaCha20 nonce
salt := make([]byte, 16)
nonce := make([]byte, 24)
if _, err := rand.Read(salt); err != nil {
panic(err)
}
if _, err := rand.Read(nonce); err != nil {
panic(err)
}
fout.Write(salt)
fout.Write(nonce)
// Generate key and XChaCha20
key := argon2.IDKey([]byte(password), salt, 4, 1<<20, 4, 32)
chacha, _ := chacha20.NewUnauthenticatedCipher(key, nonce)
// Encrypt the entire volume
done, counter := 0, 0
for {
src := make([]byte, MiB)
size, err := fin.Read(src)
if err != nil {
break
}
src = src[:size]
dst := make([]byte, len(src))
chacha.XORKeyStream(dst, src)
fout.Write(dst)
// Update stats
done += size
counter += MiB
progress = float32(float64(done) / float64(total))
giu.Update()
// Change nonce after 60 GiB to prevent overflow
if counter >= 60*GiB {
tmp := sha3.New256()
tmp.Write(nonce)
nonce = tmp.Sum(nil)[:24]
chacha, _ = chacha20.NewUnauthenticatedCipher(key, nonce)
counter = 0
}
}
fin.Close()
fout.Close()
os.Remove(fin.Name())
canCancel = true
giu.Update()
}
// Split the file into chunks
if split {
var splitted []string
stat, _ := os.Stat(outputFile)
size := stat.Size()
finishedFiles := 0
finishedBytes := 0
chunkSize, _ := strconv.Atoi(splitSize)
// Calculate chunk size
if splitSelected == 0 {
chunkSize *= KiB
} else if splitSelected == 1 {
chunkSize *= MiB
} else if splitSelected == 2 {
chunkSize *= GiB
} else if splitSelected == 3 {
chunkSize *= TiB
} else {
chunkSize = int(math.Ceil(float64(size) / float64(chunkSize)))
}
// Get the number of required chunks
chunks := int(math.Ceil(float64(size) / float64(chunkSize)))
progressInfo = fmt.Sprintf("%d/%d", finishedFiles+1, chunks)
giu.Update()
// Open the volume for reading
fin, _ := os.Open(outputFile)
// Delete existing chunks to prevent mixed chunks
names, _ := filepath.Glob(outputFile + ".*")
for _, i := range names {
os.Remove(i)
}
// Start the splitting process
startTime := time.Now()
for i := 0; i < chunks; i++ {
// Make the chunk
fout, _ := os.Create(fmt.Sprintf("%s.%d", outputFile, i))
done := 0
// Copy data into the chunk
for {
data := make([]byte, MiB)
for done+len(data) > chunkSize {
data = make([]byte, int(math.Ceil(float64(len(data))/2)))
}
read, err := fin.Read(data)
if err != nil {
break
}
if !working {
cancel(fin, fout)
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
os.Remove(outputFile)
for _, j := range splitted { // Remove existing chunks
os.Remove(j)
}
os.Remove(fmt.Sprintf("%s.%d", outputFile, i))
return
}
data = data[:read]
_, err = fout.Write(data)
if err != nil {
insufficientSpace(fin, fout)
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
os.Remove(outputFile)
for _, j := range splitted { // Remove existing chunks
os.Remove(j)
}
os.Remove(fmt.Sprintf("%s.%d", outputFile, i))
return
}
done += read
if done >= chunkSize {
break
}
// Update stats
finishedBytes += read
progress, speed, eta = statify(int64(finishedBytes), int64(size), startTime)
popupStatus = fmt.Sprintf("Splitting at %.2f MiB/s (ETA: %s)", speed, eta)
giu.Update()
}
fout.Close()
// Update stats
finishedFiles++
if finishedFiles == chunks {
finishedFiles--
}
splitted = append(splitted, fmt.Sprintf("%s.%d", outputFile, i))
progressInfo = fmt.Sprintf("%d/%d", finishedFiles+1, chunks)
giu.Update()
}
fin.Close()
os.Remove(outputFile)
}
canCancel = false
progress = 0
progressInfo = ""
giu.Update()
// Delete temporary files used during encryption and decryption
if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
if deniability {
os.Remove(strings.TrimSuffix(inputFile, ".tmp"))
}
}
// Delete the input files if the user chooses
if delete {
popupStatus = "Deleting files..."
giu.Update()
if mode == "decrypt" {
if recombine { // Remove each chunk of volume
i := 0
for {
_, err := os.Stat(fmt.Sprintf("%s.%d", inputFileOld, i))
if err != nil {
break
}
os.Remove(fmt.Sprintf("%s.%d", inputFileOld, i))
i++
}
} else {
os.Remove(inputFile)
if deniability {
os.Remove(strings.TrimSuffix(inputFile, ".tmp"))
}
}
} else {
for _, i := range onlyFiles {
os.Remove(i)
}
for _, i := range onlyFolders {
os.RemoveAll(i)
}
}
}
if mode == "decrypt" && deniability {
os.Remove(inputFile)
}
if mode == "decrypt" && !kept && autoUnzip {
showProgress = true
popupStatus = "Unzipping..."
giu.Update()
if err := unpackArchive(outputFile); err != nil {
mainStatus = "Auto unzipping failed!"
mainStatusColor = RED
giu.Update()
return
}
}
// All done, reset the UI
oldKept := kept
resetUI()
kept = oldKept
// If the user chose to keep a corrupted/modified file, let them know
if kept {
mainStatus = "The input file was modified. Please be careful"
mainStatusColor = YELLOW
} else {
mainStatus = "Completed"
mainStatusColor = GREEN
}
}
// If the OS denies reading or writing to a file
func accessDenied(s string) {
mainStatus = s + " access denied by operating system"
mainStatusColor = RED
}
// If there isn't enough disk space
func insufficientSpace(fin *os.File, fout *os.File) {
fin.Close()
fout.Close()
mainStatus = "Insufficient disk space"
mainStatusColor = RED
}
// If corruption is detected during decryption
func broken(fin *os.File, fout *os.File, message string, keepOutput bool) {
fin.Close()
fout.Close()
mainStatus = message
mainStatusColor = RED
// Clean up files since decryption failed
if recombine {
os.Remove(inputFile)
}
if !keepOutput {
os.Remove(outputFile)
}
}
// Stop working if user hits "Cancel"
func cancel(fin *os.File, fout *os.File) {
fin.Close()
fout.Close()
mainStatus = "Operation cancelled by user"
mainStatusColor = WHITE
}
// Reset the UI to a clean state with nothing selected or checked
func resetUI() {
imgui.ClearActiveID()
mode = ""
inputFile = ""
inputFileOld = ""
outputFile = ""
onlyFiles = nil
onlyFolders = nil
allFiles = nil
inputLabel = "Drop files and folders into this window"
password = ""
cpassword = ""
passwordState = giu.InputTextFlagsPassword
passwordStateLabel = "Show"
passgenLength = 32
passgenUpper = true
passgenLower = true
passgenNums = true
passgenSymbols = true
passgenCopy = true
keyfile = false
keyfiles = nil
keyfileOrdered = false
keyfileLabel = "None selected"
comments = ""
commentsLabel = "Comments:"
commentsDisabled = false
paranoid = false
reedsolo = false
deniability = false
recursively = false
split = false
splitSize = ""
splitSelected = 1
recombine = false
compress = false
delete = false
autoUnzip = false
sameLevel = false
keep = false
kept = false
startLabel = "Start"
mainStatus = "Ready"
mainStatusColor = WHITE
popupStatus = ""
progress = 0
progressInfo = ""
giu.Update()
}
// Reed-Solomon encoder
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
}
// Reed-Solomon decoder
func rsDecode(rs *infectious.FEC, data []byte) ([]byte, error) {
// If fast decode, just return the first 128 bytes
if rs.Total() == 136 && fastDecode {
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)
// Force decode the data but return the error as well
if err != nil {
if rs.Total() == 136 {
return data[:128], err
}
return data[:rs.Total()/3], err
}
// No issues, return the decoded data
return res, nil
}
// PKCS#7 pad (for use with Reed-Solomon)
func pad(data []byte) []byte {
padLen := 128 - len(data)%128
padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
return append(data, padding...)
}
// PKCS#7 unpad
func unpad(data []byte) []byte {
padLen := int(data[127])
return data[:128-padLen]
}
// Generate a cryptographically secure password
func genPassword() string {
chars := ""
if passgenUpper {
chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
}
if passgenLower {
chars += "abcdefghijklmnopqrstuvwxyz"
}
if passgenNums {
chars += "1234567890"
}
if passgenSymbols {
chars += "-=_+!@#$^&()?<>"
}
tmp := make([]byte, passgenLength)
for i := 0; i < int(passgenLength); i++ {
j, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars))))
tmp[i] = chars[j.Int64()]
}
if passgenCopy {
giu.Context.GetPlatform().SetClipboard(string(tmp))
}
return string(tmp)
}
// Convert done, total, and starting time to progress, speed, and ETA
func statify(done int64, total int64, start time.Time) (float32, float64, string) {
progress := float32(done) / float32(total)
elapsed := float64(time.Since(start)) / float64(MiB) / 1000
speed := float64(done) / elapsed / float64(MiB)
eta := int(math.Floor(float64(total-done) / (speed * float64(MiB))))
return float32(math.Min(float64(progress), 1)), speed, timeify(eta)
}
// Convert seconds to HH:MM:SS
func timeify(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)
}
// Convert bytes to KiB, MiB, etc.
func sizeify(size int64) string {
if size >= int64(TiB) {
return fmt.Sprintf("%.2f TiB", float64(size)/float64(TiB))
} else if size >= int64(GiB) {
return fmt.Sprintf("%.2f GiB", float64(size)/float64(GiB))
} else if size >= int64(MiB) {
return fmt.Sprintf("%.2f MiB", float64(size)/float64(MiB))
} else {
return fmt.Sprintf("%.2f KiB", float64(size)/float64(KiB))
}
}
func unpackArchive(zipPath string) error {
// Open the .zip
reader, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer reader.Close()
// Calculate total unpack size by summing each entrys UncompressedSize64
var totalSize int64
for _, f := range reader.File {
totalSize += int64(f.UncompressedSize64)
}
// Directory containing the .zip
extractDir := filepath.Dir(zipPath)
// Setup progress tracking
var done int64
startTime := time.Now()
// Iterate over each file in the archive
for i, f := range reader.File {
// If user clicked "Cancel" elsewhere, stop
if !working {
return errors.New("operation canceled by user")
}
outPath := filepath.Join(extractDir, f.Name)
// Make directory if current entry is a folder
if f.FileInfo().IsDir() {
if err := os.MkdirAll(outPath, f.Mode()); err != nil {
return err
}
continue
}
// Otherwise create necessary parent directories
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
return err
}
// Open the file inside the archive
fileInArchive, err := f.Open()
if err != nil {
return err
}
defer fileInArchive.Close()
// Create/overwrite the destination file
dstFile, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
// Read from zip in chunks to update progress
buffer := make([]byte, MiB) // e.g., 1 MiB chunk
for {
if !working {
dstFile.Close()
os.Remove(outPath) // remove partial extraction if canceled
return errors.New("operation canceled by user")
}
n, readErr := fileInArchive.Read(buffer)
if n > 0 {
_, writeErr := dstFile.Write(buffer[:n])
if writeErr != nil {
dstFile.Close()
return writeErr
}
// Update "done" and recalc progress, speed, eta
done += int64(n)
progress, speed, eta = statify(done, totalSize, startTime)
progressInfo = fmt.Sprintf("%d/%d", i+1, len(reader.File))
popupStatus = fmt.Sprintf("Unpacking at %.2f MiB/s (ETA: %s)", speed, eta)
giu.Update()
}
if readErr != nil {
if readErr == io.EOF {
break
}
dstFile.Close()
return readErr
}
}
dstFile.Close()
}
return nil
}
func main() {
// Create the main window
window = giu.NewMasterWindow("Picocrypt "+version[1:], 318, 507, giu.MasterWindowFlagsNotResizable)
// Start the dialog module
dialog.Init()
// Set callbacks
window.SetDropCallback(onDrop)
window.SetCloseCallback(func() bool {
return !working && !showProgress
})
// Set universal DPI
dpi = giu.Context.GetPlatform().GetContentScale()
// Simulate dropping command line arguments
flag.Parse()
if flag.NArg() > 0 {
onDrop(flag.Args())
}
// Start the UI
window.Run(draw)
}