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 // Advanced options
var paranoid bool var paranoid bool
var reedsolo bool var reedsolo bool
var deniability bool
var recursively bool
var split bool var split bool
var splitSize string var splitSize string
var splitUnits = []string{"KiB", "MiB", "GiB", "TiB", "Total"} var splitUnits = []string{"KiB", "MiB", "GiB", "TiB", "Total"}
@ -412,7 +414,7 @@ func draw() {
), ),
giu.Dummy(0, 0), giu.Dummy(0, 0),
giu.Style().SetDisabled(mode == "decrypt" && !keyfile).To( giu.Style().SetDisabled(mode == "decrypt" && !keyfile && !deniability).To(
giu.Row( giu.Row(
giu.Label("Keyfiles:"), giu.Label("Keyfiles:"),
giu.Button("Edit").Size(54, 0).OnClick(func() { giu.Button("Edit").Size(54, 0).OnClick(func() {
@ -484,16 +486,18 @@ func draw() {
giu.Checkbox("Paranoid mode", &paranoid), giu.Checkbox("Paranoid mode", &paranoid),
giu.Tooltip("Provides the highest level of security attainable."), giu.Tooltip("Provides the highest level of security attainable."),
giu.Dummy(-170, 0), giu.Dummy(-170, 0),
giu.Checkbox("Compress files", &compress).OnChange(func() { giu.Style().SetDisabled(recursively).To(
if !(len(allFiles) > 1 || len(onlyFolders) > 0) { giu.Checkbox("Compress files", &compress).OnChange(func() {
if compress { if !(len(allFiles) > 1 || len(onlyFolders) > 0) {
outputFile = filepath.Join(filepath.Dir(outputFile), "Encrypted") + ".zip.pcv" if compress {
} else { outputFile = filepath.Join(filepath.Dir(outputFile), "Encrypted") + ".zip.pcv"
outputFile = filepath.Join(filepath.Dir(outputFile), filepath.Base(inputFile)) + ".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() ).Build()
giu.Row( giu.Row(
@ -504,6 +508,18 @@ func draw() {
giu.Tooltip("Delete the input files after encryption."), giu.Tooltip("Delete the input files after encryption."),
).Build() ).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.Row(
giu.Checkbox("Split into chunks:", &split), giu.Checkbox("Split into chunks:", &split),
giu.Tooltip("Split the output file into smaller chunks."), giu.Tooltip("Split the output file into smaller chunks."),
@ -517,8 +533,10 @@ func draw() {
).Build() ).Build()
} else { } else {
giu.Row( giu.Row(
giu.Checkbox("Force decrypt", &keep), giu.Style().SetDisabled(deniability).To(
giu.Tooltip("Override security measures when decrypting."), giu.Checkbox("Force decrypt", &keep),
giu.Tooltip("Override security measures when decrypting."),
),
giu.Dummy(-170, 0), giu.Dummy(-170, 0),
giu.Checkbox("Delete volume", &delete), giu.Checkbox("Delete volume", &delete),
giu.Tooltip("Delete the volume after a successful decryption."), giu.Tooltip("Delete the volume after a successful decryption."),
@ -526,77 +544,87 @@ func draw() {
} }
}), }),
giu.Label("Save output as:"), giu.Style().SetDisabled(recursively).To(
giu.Custom(func() { giu.Label("Save output as:"),
w, _ := giu.GetAvailableRegion() giu.Custom(func() {
bw, _ := giu.CalcTextSize("Change") w, _ := giu.GetAvailableRegion()
p, _ := giu.GetWindowPadding() bw, _ := giu.CalcTextSize("Change")
bw += p * 2 p, _ := giu.GetWindowPadding()
dw := w - bw - p bw += p * 2
giu.Style().SetDisabled(true).To( dw := w - bw - p
giu.InputText(func() *string { giu.Style().SetDisabled(true).To(
tmp := "" giu.InputText(func() *string {
if outputFile == "" { tmp := ""
if outputFile == "" {
return &tmp
}
tmp = filepath.Base(outputFile)
if split {
tmp += ".*"
}
if recursively {
tmp = "(multiple values)"
}
return &tmp 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 { // Get the chosen file path
tmp += ".*" file, err := f.Save()
if file == "" || err != nil {
return
} }
return &tmp file = filepath.Join(filepath.Dir(file), strings.Split(filepath.Base(file), ".")[0])
}()).Size(dw / dpi / dpi).Flags(16384),
).Build()
giu.SameLine() // Add the correct extensions
giu.Button("Change").Size(bw/dpi, 0).OnClick(func() { if mode == "encrypt" {
f := dialog.File().Title("Choose where to save the output. Don't include extensions.") if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
f.SetStartDir(func() string { file += ".zip.pcv"
if len(onlyFiles) > 0 { } else {
return filepath.Dir(onlyFiles[0]) file += filepath.Ext(inputFile) + ".pcv"
} }
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 { } 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 { outputFile = file
if strings.HasSuffix(inputFile, ".zip.pcv") { mainStatus = "Ready."
file += ".zip" mainStatusColor = WHITE
} else { }).Build()
tmp := strings.TrimSuffix(filepath.Base(inputFile), ".pcv") giu.Tooltip("Save the output with a custom name and path.").Build()
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.Dummy(0, 0),
giu.Separator(), giu.Separator(),
giu.Dummy(0, 0), 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 { if keyfile && keyfiles == nil {
mainStatus = "Please select your keyfiles." mainStatus = "Please select your keyfiles."
mainStatusColor = RED mainStatusColor = RED
@ -623,7 +651,7 @@ func draw() {
} }
// If files already exist, show the overwrite modal // If files already exist, show the overwrite modal
if err == nil { if err == nil && !recursively {
showOverwrite = true showOverwrite = true
modalId++ modalId++
giu.Update() giu.Update()
@ -633,12 +661,64 @@ func draw() {
canCancel = true canCancel = true
modalId++ modalId++
giu.Update() giu.Update()
go func() { if !recursively {
work() go func() {
working = false work()
showProgress = false working = false
giu.Update() 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.Style().SetColor(giu.StyleColorText, mainStatusColor).To(
@ -771,59 +851,58 @@ func onDrop(names []string) {
return 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) tmp := make([]byte, 15)
fin.Read(tmp) fin.Read(tmp)
tmp, err = rsDecode(rs5, tmp) tmp, err = rsDecode(rs5, tmp)
if valid, _ := regexp.Match(`^v1\.\d{2}`, tmp); !valid || err != nil { if valid, _ := regexp.Match(`^v1\.\d{2}`, tmp); !valid || err != nil {
resetUI() // Volume has plausible deniability
mainStatus = "This doesn't seem like a Picocrypt volume." deniability = true
mainStatusColor = RED mainStatus = "Can't read header, assuming volume is deniable."
fin.Close() fin.Close()
return } else {
} // Read comments from file and check for corruption
tmp = make([]byte, 15)
// 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) fin.Read(tmp)
comments = "" tmp, err = rsDecode(rs5, tmp)
for i := 0; i < commentsLength*3; i += 3 { if err == nil {
t, err := rsDecode(rs1, tmp[i:i+3]) commentsLength, _ := strconv.Atoi(string(tmp))
if err != nil { tmp = make([]byte, commentsLength*3)
comments = "Comments are corrupted." fin.Read(tmp)
break 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 // Read flags from file and check for corruption
flags := make([]byte, 15) flags := make([]byte, 15)
fin.Read(flags) fin.Read(flags)
fin.Close() fin.Close()
flags, err = rsDecode(rs5, flags) flags, err = rsDecode(rs5, flags)
if err != nil { if err != nil {
mainStatus = "The volume header is damaged." mainStatus = "The volume header is damaged."
mainStatusColor = RED mainStatusColor = RED
return return
} }
// Update UI and variables according to flags // Update UI and variables according to flags
if flags[1] == 1 { if flags[1] == 1 {
keyfile = true keyfile = true
keyfileLabel = "Keyfiles required." keyfileLabel = "Keyfiles required."
} else { } else {
keyfileLabel = "Not applicable." keyfileLabel = "Not applicable."
} }
if flags[2] == 1 { if flags[2] == 1 {
keyfileOrdered = true keyfileOrdered = true
}
} }
} else { // One file was dropped for encryption } else { // One file was dropped for encryption
mode = "encrypt" mode = "encrypt"
@ -1087,6 +1166,85 @@ func work() {
inputFile = outputFile + ".pcv" 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 canCancel = false
progress = 0 progress = 0
progressInfo = "" progressInfo = ""
@ -1232,6 +1390,10 @@ func work() {
paranoid = flags[0] == 1 paranoid = flags[0] == 1
reedsolo = flags[3] == 1 reedsolo = flags[3] == 1
padded = flags[4] == 1 padded = flags[4] == 1
if deniability {
keyfile = flags[1] == 1
keyfileOrdered = flags[2] == 1
}
salt = make([]byte, 48) salt = make([]byte, 48)
fin.Read(salt) fin.Read(salt)
@ -1365,7 +1527,7 @@ func work() {
keyCorrect := subtle.ConstantTimeCompare(keyHash, keyHashRef) == 1 keyCorrect := subtle.ConstantTimeCompare(keyHash, keyHashRef) == 1
keyfileCorrect := subtle.ConstantTimeCompare(keyfileHash, keyfileHashRef) == 1 keyfileCorrect := subtle.ConstantTimeCompare(keyfileHash, keyfileHashRef) == 1
incorrect := !keyCorrect incorrect := !keyCorrect
if keyfile { if keyfile || len(keyfiles) > 0 {
incorrect = !keyCorrect || !keyfileCorrect incorrect = !keyCorrect || !keyfileCorrect
} }
@ -1382,8 +1544,16 @@ func work() {
} else { } else {
mainStatus = "Incorrect keyfiles." mainStatus = "Incorrect keyfiles."
} }
if deniability {
fin.Close()
os.Remove(inputFile)
inputFile = strings.TrimSuffix(inputFile, ".tmp")
}
} }
broken(fin, nil, mainStatus, true) broken(fin, nil, mainStatus, true)
if recombine {
inputFile = inputFileOld
}
return return
} }
} }
@ -1648,6 +1818,69 @@ func work() {
fin.Close() fin.Close()
fout.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 // Split the file into chunks
if split { if split {
var splitted []string var splitted []string
@ -1764,6 +1997,9 @@ func work() {
// Delete temporary files used during encryption and decryption // Delete temporary files used during encryption and decryption
if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress { if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile) os.Remove(inputFile)
if deniability {
os.Remove(strings.TrimSuffix(inputFile, ".tmp"))
}
} }
// Delete the input files if the user chooses // Delete the input files if the user chooses
@ -1784,6 +2020,9 @@ func work() {
} }
} else { } else {
os.Remove(inputFile) os.Remove(inputFile)
if deniability {
os.Remove(strings.TrimSuffix(inputFile, ".tmp"))
}
} }
} else { } else {
for _, i := range onlyFiles { for _, i := range onlyFiles {
@ -1794,6 +2033,9 @@ func work() {
} }
} }
} }
if mode == "decrypt" && deniability {
os.Remove(inputFile)
}
// All done, reset the UI // All done, reset the UI
oldKept := kept oldKept := kept
@ -1884,6 +2126,8 @@ func resetUI() {
paranoid = false paranoid = false
reedsolo = false reedsolo = false
deniability = false
recursively = false
split = false split = false
splitSize = "" splitSize = ""
splitSelected = 1 splitSelected = 1
@ -2013,7 +2257,7 @@ func sizeify(size int64) string {
func main() { func main() {
// Create the main window // Create the main window
window = giu.NewMasterWindow("Picocrypt", 318, 479, giu.MasterWindowFlagsNotResizable) window = giu.NewMasterWindow("Picocrypt", 318, 507, giu.MasterWindowFlagsNotResizable)
// Start the dialog module // Start the dialog module
dialog.Init() dialog.Init()