This commit is contained in:
Evan Su 2025-04-10 05:38:01 +00:00 committed by GitHub
commit 374c946ff7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 165 additions and 102 deletions

View file

@ -3,6 +3,12 @@
<li>Migrate golang.org/x/crypto to standard library imports (https://github.com/golang/go/issues/65269)</li> <li>Migrate golang.org/x/crypto to standard library imports (https://github.com/golang/go/issues/65269)</li>
</ul> </ul>
# v1.48 (Released 04/11/2025)
<ul>
<li>✓ Allow pressing 'Enter' key to press Start/Process button</li>
<li>✓ Warn user when encrypting multiple files to an external drive</li>
</ul>
# v1.47 (Released 02/19/2025) # v1.47 (Released 02/19/2025)
<ul> <ul>
<li>✓ No code changes, just build on newly released Go 1.24</li> <li>✓ No code changes, just build on newly released Go 1.24</li>

View file

@ -92,6 +92,14 @@ While being simple, Picocrypt also strives to be powerful in the hands of knowle
<li><strong>Recursively</strong>: If you want to encrypt and/or decrypt a large set of files individually, this option will tell Picocrypt to go through every recursive file that you drop in and encrypt/decrypt it separately. This is useful, for example, if you are encrypting thousands of large documents and want to be able to decrypt any one of them in particular without having to download and decrypt the entire set of documents. Keep in mind that this is a very complex feature that should only be used if you know what you are doing.</li> <li><strong>Recursively</strong>: If you want to encrypt and/or decrypt a large set of files individually, this option will tell Picocrypt to go through every recursive file that you drop in and encrypt/decrypt it separately. This is useful, for example, if you are encrypting thousands of large documents and want to be able to decrypt any one of them in particular without having to download and decrypt the entire set of documents. Keep in mind that this is a very complex feature that should only be used if you know what you are doing.</li>
</ul> </ul>
# Caveats
When encrypting multiple files, Picocrypt will automatically zip them into one file before encrypting it. However, this requires a two-step process that creates an unencrypted temporary `.zip.tmp` file in the same destination folder. This has two implications:
<ol>
<li>There must be at least double the available free space on the target drive as the combined total size of input files</li>
<li>The target drive must be safe to save confidential data; if not, the unencrypted temporary file may be recoverable even after deletion</li>
</ol>
To mitigate these caveats, Picocrypt will show info and warning labels accordingly. However, it is best to prevent these issues altogether <strong>by always encrypting and decrypting on your main host drive</strong> and then copying encrypted volumes to and from external sources, <strong>or zipping up input files beforehand and encrypting that single file</strong> which doesn't have these caveats.
# Security # Security
For more information on how Picocrypt handles cryptography, see <a href="Internals.md">Internals</a> for the technical details. If you're worried about the safety of me or this project, let me assure you that this repository won't be hijacked or backdoored. I have 2FA (TOTP) enabled on all accounts with a tie to Picocrypt (GitHub, Reddit, Google, etc.), in addition to full-disk encryption on all of my portable devices. For further hardening, Picocrypt uses my isolated forks of dependencies and I fetch upstream only when I have taken a look at the changes and believe that there aren't any security issues. This means that if a dependency gets hacked or deleted by the author, Picocrypt will be using my fork of it and remain completely unaffected. You can feel confident about using Picocrypt as long as you understand: For more information on how Picocrypt handles cryptography, see <a href="Internals.md">Internals</a> for the technical details. If you're worried about the safety of me or this project, let me assure you that this repository won't be hijacked or backdoored. I have 2FA (TOTP) enabled on all accounts with a tie to Picocrypt (GitHub, Reddit, Google, etc.), in addition to full-disk encryption on all of my portable devices. For further hardening, Picocrypt uses my isolated forks of dependencies and I fetch upstream only when I have taken a look at the changes and believe that there aren't any security issues. This means that if a dependency gets hacked or deleted by the author, Picocrypt will be using my fork of it and remain completely unaffected. You can feel confident about using Picocrypt as long as you understand:

View file

@ -1 +1 @@
1.47 1.48

View file

@ -2,7 +2,7 @@ package main
/* /*
Picocrypt v1.47 Picocrypt v1.48
Copyright (c) Evan Su Copyright (c) Evan Su
Released under a GNU GPL v3 License Released under a GNU GPL v3 License
https://github.com/Picocrypt/Picocrypt https://github.com/Picocrypt/Picocrypt
@ -30,6 +30,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -60,7 +61,7 @@ var TRANSPARENT = color.RGBA{0x00, 0x00, 0x00, 0x00}
// Generic variables // Generic variables
var window *giu.MasterWindow var window *giu.MasterWindow
var version = "v1.47" var version = "v1.48"
var dpi float32 var dpi float32
var mode string var mode string
var working bool var working bool
@ -131,6 +132,9 @@ var mainStatus = "Ready"
var mainStatusColor = WHITE var mainStatusColor = WHITE
var popupStatus string var popupStatus string
var temporaryZip bool
var externalDst bool
// Progress variables // Progress variables
var progress float32 var progress float32
var progressInfo string var progressInfo string
@ -173,10 +177,117 @@ func (p *compressorProgress) Read(data []byte) (int, error) {
return read, err return read, err
} }
var onClickStartButton = func() {
// Start button should be disabled if these conditions are true; don't do anything if so
if (len(keyfiles) == 0 && password == "") || (mode == "encrypt" && password != cpassword) {
return
}
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()
}()
}
}
}
// The main user interface // The main user interface
func draw() { func draw() {
giu.SingleWindow().Flags(524351).Layout( giu.SingleWindow().Flags(524351).Layout(
giu.Custom(func() { giu.Custom(func() {
if giu.IsKeyReleased(giu.KeyEnter) {
onClickStartButton()
return
}
if showPassgen { if showPassgen {
giu.PopupModal("Generate password:##"+strconv.Itoa(modalId)).Flags(6).Layout( giu.PopupModal("Generate password:##"+strconv.Itoa(modalId)).Flags(6).Layout(
giu.Row( giu.Row(
@ -499,7 +610,7 @@ 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.Style().SetDisabled(recursively).To( giu.Style().SetDisabled(recursively || !(len(allFiles) > 1 || len(onlyFolders) > 0)).To(
giu.Checkbox("Compress files", &compress).OnChange(func() { giu.Checkbox("Compress files", &compress).OnChange(func() {
if !(len(allFiles) > 1 || len(onlyFolders) > 0) { if !(len(allFiles) > 1 || len(onlyFolders) > 0) {
if compress { if compress {
@ -629,6 +740,21 @@ func draw() {
} else { } else {
file += filepath.Ext(inputFile) + ".pcv" file += filepath.Ext(inputFile) + ".pcv"
} }
externalDst = false
GOOS := strings.ToLower(runtime.GOOS)
if strings.HasPrefix(GOOS, "windows") {
if !strings.HasPrefix(file, "C:") {
externalDst = true
}
} else if strings.HasPrefix(GOOS, "linux") {
if strings.Contains(file, "/media/") || strings.Contains(file, "/mnt/") {
externalDst = true
}
} else if strings.HasPrefix(GOOS, "darwin") {
if strings.Contains(file, "/Volumes/") {
externalDst = true
}
}
} else { } else {
if strings.HasSuffix(inputFile, ".zip.pcv") { if strings.HasSuffix(inputFile, ".zip.pcv") {
file += ".zip" file += ".zip"
@ -653,106 +779,26 @@ func draw() {
return startLabel return startLabel
} }
return "Process" return "Process"
}()).Size(giu.Auto, 34).OnClick(func() { }()).Size(giu.Auto, 34).OnClick(onClickStartButton),
if keyfile && keyfiles == nil { giu.Custom(func() {
mainStatus = "Please select your keyfiles" if temporaryZip && externalDst {
mainStatusColor = RED giu.Style().SetColor(giu.StyleColorText, YELLOW).To(
return giu.Label("Warning: unencrypted temp files will be created"),
} ).Build()
tmp, err := strconv.Atoi(splitSize) } else if temporaryZip {
if split && (splitSize == "" || tmp <= 0 || err != nil) { giu.Style().SetColor(giu.StyleColorText, WHITE).To(
mainStatus = "Invalid chunk size" giu.Label(mainStatus + " (info: will create temporary files)"),
mainStatusColor = RED ).Build()
return } else if externalDst {
} giu.Style().SetColor(giu.StyleColorText, WHITE).To(
giu.Label(mainStatus + " (info: target may be an external drive)"),
// Check if output file already exists ).Build()
_, err = os.Stat(outputFile) } else {
giu.Style().SetColor(giu.StyleColorText, mainStatusColor).To(
// Check if any split chunks already exist giu.Label(mainStatus),
if split { ).Build()
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() { giu.Custom(func() {
@ -989,6 +1035,7 @@ func onDrop(names []string) {
// Set the input and output paths // Set the input and output paths
inputFile = filepath.Join(filepath.Dir(names[0]), "Encrypted") + ".zip" inputFile = filepath.Join(filepath.Dir(names[0]), "Encrypted") + ".zip"
outputFile = inputFile + ".pcv" outputFile = inputFile + ".pcv"
temporaryZip = true
} }
// Recursively add all files in 'onlyFolders' to 'allFiles' // Recursively add all files in 'onlyFolders' to 'allFiles'
@ -2251,6 +2298,8 @@ func resetUI() {
mainStatus = "Ready" mainStatus = "Ready"
mainStatusColor = WHITE mainStatusColor = WHITE
popupStatus = "" popupStatus = ""
temporaryZip = false
externalDst = false
progress = 0 progress = 0
progressInfo = "" progressInfo = ""