Add features

This commit is contained in:
Evan Su 2023-04-27 22:52:01 -04:00 committed by GitHub
parent 71b03ef7d4
commit 71bd746f9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 369 additions and 125 deletions

View File

@ -109,6 +109,8 @@ 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"}
@ -412,7 +414,7 @@ func draw() {
),
giu.Dummy(0, 0),
giu.Style().SetDisabled(mode == "decrypt" && !keyfile).To(
giu.Style().SetDisabled(mode == "decrypt" && !keyfile && !deniability).To(
giu.Row(
giu.Label("Keyfiles:"),
giu.Button("Edit").Size(54, 0).OnClick(func() {
@ -484,16 +486,18 @@ func draw() {
giu.Checkbox("Paranoid mode", &paranoid),
giu.Tooltip("Provides the highest level of security attainable."),
giu.Dummy(-170, 0),
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.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."),
}),
giu.Tooltip("Compress files with Deflate before encrypting."),
),
).Build()
giu.Row(
@ -504,6 +508,18 @@ func draw() {
giu.Tooltip("Delete the input files after encryption."),
).Build()
giu.Row(
giu.Checkbox("Deniability", &deniability),
giu.Tooltip("Add plausible deniability to the volume."),
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."),
@ -517,8 +533,10 @@ func draw() {
).Build()
} else {
giu.Row(
giu.Checkbox("Force decrypt", &keep),
giu.Tooltip("Override security measures when decrypting."),
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."),
@ -526,77 +544,87 @@ func draw() {
}
}),
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 == "" {
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")
}
tmp = filepath.Base(outputFile)
if split {
tmp += ".*"
// Get the chosen file path
file, err := f.Save()
if file == "" || err != nil {
return
}
return &tmp
}()).Size(dw / dpi / dpi).Flags(16384),
).Build()
file = filepath.Join(filepath.Dir(file), strings.Split(filepath.Base(file), ".")[0])
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"
// 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 {
file += filepath.Ext(inputFile) + ".pcv"
if strings.HasSuffix(inputFile, ".zip.pcv") {
file += ".zip"
} else {
tmp := strings.TrimSuffix(filepath.Base(inputFile), ".pcv")
file += filepath.Ext(tmp)
}
}
} 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()
}),
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(startLabel).Size(giu.Auto, 34).OnClick(func() {
giu.Button(func() string {
if !recursively {
return startLabel
}
return "Work"
}()).Size(giu.Auto, 34).OnClick(func() {
if keyfile && keyfiles == nil {
mainStatus = "Please select your keyfiles."
mainStatusColor = RED
@ -623,7 +651,7 @@ func draw() {
}
// If files already exist, show the overwrite modal
if err == nil {
if err == nil && !recursively {
showOverwrite = true
modalId++
giu.Update()
@ -633,12 +661,64 @@ func draw() {
canCancel = true
modalId++
giu.Update()
go func() {
work()
working = false
showProgress = false
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(
@ -771,59 +851,58 @@ func onDrop(names []string) {
return
}
// Use regex to test if the input is a valid Picocrypt volume
// Check if version can be read from header
tmp := make([]byte, 15)
fin.Read(tmp)
tmp, err = rsDecode(rs5, tmp)
if valid, _ := regexp.Match(`^v1\.\d{2}`, tmp); !valid || err != nil {
resetUI()
mainStatus = "This doesn't seem like a Picocrypt volume."
mainStatusColor = RED
// Volume has plausible deniability
deniability = true
mainStatus = "Can't read header, assuming volume is deniable."
fin.Close()
return
}
// 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)
} else {
// Read comments from file and check for corruption
tmp = make([]byte, 15)
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
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)
}
comments += string(t)
} else {
comments = "Comments are corrupted."
}
} 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
}
// 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
// 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"
@ -1087,6 +1166,85 @@ func work() {
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 = ""
@ -1232,6 +1390,10 @@ func work() {
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)
@ -1365,7 +1527,7 @@ func work() {
keyCorrect := subtle.ConstantTimeCompare(keyHash, keyHashRef) == 1
keyfileCorrect := subtle.ConstantTimeCompare(keyfileHash, keyfileHashRef) == 1
incorrect := !keyCorrect
if keyfile {
if keyfile || len(keyfiles) > 0 {
incorrect = !keyCorrect || !keyfileCorrect
}
@ -1382,8 +1544,16 @@ func work() {
} 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
}
}
@ -1648,6 +1818,69 @@ func work() {
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)
rand.Read(salt)
rand.Read(nonce)
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
@ -1764,6 +1997,9 @@ func work() {
// 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
@ -1784,6 +2020,9 @@ func work() {
}
} else {
os.Remove(inputFile)
if deniability {
os.Remove(strings.TrimSuffix(inputFile, ".tmp"))
}
}
} else {
for _, i := range onlyFiles {
@ -1794,6 +2033,9 @@ func work() {
}
}
}
if mode == "decrypt" && deniability {
os.Remove(inputFile)
}
// All done, reset the UI
oldKept := kept
@ -1884,6 +2126,8 @@ func resetUI() {
paranoid = false
reedsolo = false
deniability = false
recursively = false
split = false
splitSize = ""
splitSelected = 1
@ -2013,7 +2257,7 @@ func sizeify(size int64) string {
func main() {
// Create the main window
window = giu.NewMasterWindow("Picocrypt", 318, 479, giu.MasterWindowFlagsNotResizable)
window = giu.NewMasterWindow("Picocrypt", 318, 507, giu.MasterWindowFlagsNotResizable)
// Start the dialog module
dialog.Init()