This commit is contained in:
Evan Su 2025-04-18 19:03:36 +00:00 committed by GitHub
commit 422e91fe9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 368 additions and 220 deletions

View file

@ -12,11 +12,11 @@ body:
- type: markdown
attributes:
value: |
Picocrypt is a "finished" piece of software and is in a maintenance-only stage. This does not mean the software is old, outdated, or abandonware, but that the focus is on fixing bugs and ensuring the software continues to work smoothly as opposed to actively developing new features. As well, my time as the developer is limited considering that Picocrypt brings me no monetary benefit and is entirely a gift of my time and skill to the community.
Picocrypt is a "finished" piece of software and is in a maintenance-only stage. This does not mean the software is old, outdated, or abandonware, but that the sole focus is on fixing bugs and ensuring the software continues to work smoothly as opposed to actively developing new features. As well, my time as the developer is very limited considering that Picocrypt brings me no monetary benefit and is entirely a gift of my time and skill to the community.
- type: markdown
attributes:
value: |
*To ensure that issues remain relevant and as time-efficient as possible for me, please follow the guidelines below depending on the type/topic of your issue.*
*Therefore, to save me time so that I can focus on the important things, please follow the guidelines below depending on your topic.*
- type: markdown
attributes:
value: |
@ -24,7 +24,7 @@ body:
- type: markdown
attributes:
value: |
These are high-priority issues and the main purpose of this repository's issue tracker. Make the issue with a short description, and then once the issue is created, add a comment with as many details as possible. Ping me (@HACKERALERT) in the comment so that I can get to it as soon as possible. Keep in mind that I define "bug" as something wrong with Picocrypt's code itself. If it's not Picocrypt's fault, it's not a bug.
These are important; make the issue with a short description, and then once the issue is created, add a comment with as many details as possible. Ping me (@HACKERALERT) in the comment so that I can get to it as soon as possible. Keep in mind that I define "bug" as something wrong with Picocrypt's code itself. If it's not Picocrypt's fault, it's not a bug.
- type: markdown
attributes:
value: |
@ -32,7 +32,7 @@ body:
- type: markdown
attributes:
value: |
Usually these issues are not directly caused by Picocrypt's code. Create the issue and in a separate comment, provide details about the environment you're running in (like OS, DE, etc.). **Do not ping me initially.** Let the issue sit for at least *3 days* to allow other users to potentially help you resolve the issue. If after 3 days, you haven't figured things out, then you may ping me (@HACKERALERT).
Usually these issues are not directly caused by Picocrypt's code. If you're on Windows, see [here](https://github.com/Picocrypt/Picocrypt/issues/91). If you're on Linux, install some packages and try again (see [here](https://github.com/Picocrypt/Picocrypt/tree/main/src#1-prerequisites)). Picocrypt only targets Windows 11, Ubuntu 24/Debian 12, and macOS 15 or later, so *do not create an issue if your OS is older than those; that is your problem, not mine*. If none of the points above help, create the issue and in a separate comment, provide details about the environment you're running in (like OS, DE, etc.). **Do not ping me initially.** Let the issue sit for at least *5 days* to allow other users to potentially help you resolve the issue. If after 5 days, you haven't figured things out, then you may ping me (@HACKERALERT).
- type: markdown
attributes:
value: |
@ -40,7 +40,7 @@ body:
- type: markdown
attributes:
value: |
These are unpreventable; Picocrypt is cryptography, file deletion, and passwords bundled into an executable... which looks similar to ransomware, unfortunately. Please report these false positives to your antivirus software provider and do not create an issue about it.
These are unpreventable; report them as false positives to your antivirus software provider and **do not create an issue about it**.
- type: markdown
attributes:
value: |
@ -48,7 +48,7 @@ body:
- type: markdown
attributes:
value: |
Create the issue and ask your question or support request in a separate comment. **Do not ping me initially.** Let the issue sit for at least *5 days* to give other users a chance to help you first. If after 5 days, you have not received any assistance, then you may ping me (@HACKERALERT).
Create the issue and ask your question or support request in a separate comment. **Do not ping me initially.** Let the issue sit for at least *10 days* to give other users a chance to help you first. If after 10 days, you have not received any assistance, then you may ping me (@HACKERALERT).
- type: markdown
attributes:
value: |
@ -56,7 +56,7 @@ body:
- type: markdown
attributes:
value: |
Picocrypt is mature software; I do not intend to add any new features. Generally, do not create any feature requests unless it's very minor and can be implemented with low effort and minimal impact on reliability and security. What is considered "minor" is subjective, but here is an example: "the ability to decrypt a volume entirely in-memory" is pretty significant, while "auto start encryption on pressing the Enter key" is relatively minor. A proof-of-concept link to code or a fork would be appreciated.
Picocrypt is mature software; I do not intend to add any new features. **Do not create these types of issues.**
- type: markdown
attributes:
value: |
@ -64,7 +64,7 @@ body:
- type: markdown
attributes:
value: |
Picocrypt prioritizes correctness and reliability over performance, so many parts of the code are written sequentially and don't use concurrency. This is intentional and need not be pointed out. Unless performance is absolutely atrocious to the point where it is indicative of a potential bug, do not make issues about performance.
Picocrypt prioritizes correctness and reliability over performance, so many parts of the code are written sequentially and don't use concurrency. This is intentional and need not be pointed out. Unless performance is absolutely atrocious to the point where it is indicative of a potential bug, **do not make issues about performance**.
- type: markdown
attributes:
value: |
@ -72,8 +72,9 @@ body:
- type: markdown
attributes:
value: |
You will have to use your best judgement here. Read the sections above to get an idea of what I expect to see and do what you think is best. Ideally, ping me only if sufficient time has passed for other users to assist/answer you, or it is best addressed by me directly.
You will have to use your best judgement here. Read the sections above to get an idea of what I expect to see and do what you think is best. Ideally, ping me only if sufficient time has passed for other users to assist/answer you, or it is best addressed by me directly. You must first look through existing issues or do a web search (AI can help!) before creating the issue. While I am allowing these generic issues to be made, if they become a hassle, I reserve the right to disallow them in the future.
- type: checkboxes
id: confirmation
attributes:
label: "Please confirm:"
options:
@ -83,7 +84,12 @@ body:
required: true
- label: "I acknowledge my issue may be ignored or closed without explanation"
required: true
- label: "I have looked through previous issues and related info already"
required: true
- label: "I will remember to close my issue when it is resolved"
required: true
- type: input
id: summary
attributes:
label: "Describe the issue briefly in a few sentences:"
description: "You can add more details in a separate comment after creating the issue."

21
.github/workflows/close-issues.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View file

@ -20,3 +20,5 @@
# Go workspace file
go.work
go.work.sum
TODO

View file

@ -1,6 +1,13 @@
# Future
# v1.48 (Released 04/18/2025)
<ul>
<li>Migrate golang.org/x/crypto to standard library imports (https://github.com/golang/go/issues/65269)</li>
<li>✓ Allow pressing 'Enter' key to press Start/Process button</li>
<li>✓ Update "Encrypt" button to "Zip and Encrypt" if multiple files</li>
<li>✓ Give user estimated required free disk space in status label</li>
<li>✓ Encrypt previously unencrypted temporary zip files</li>
<li>✓ Add `.incomplete` to filenames while work is in progress</li>
<li>✓ Use `encrypted-*.zip.pcv` output name instead of `Encrypted.zip.pcv`</li>
<li>✓ Use 0700 permissions when auto unzipping and creating folders</li>
<li>✓ Handle many more errors in the code where they were ignored previously</li>
</ul>
# v1.47 (Released 02/19/2025)

View file

@ -18,8 +18,6 @@ Picocrypt is a very small (hence <i>Pico</i>), very simple, yet very secure encr
## Windows
Picocrypt for Windows is as simple as it gets. To download the latest, standalone, and portable executable for Windows, click <a href="https://github.com/Picocrypt/Picocrypt/releases/latest/download/Picocrypt.exe">here</a>. If Microsoft Defender or your antivirus flags Picocrypt as a virus, please do your part and submit it as a false positive for the betterment of everyone.
If you use Picocrypt frequently, you can download an installer <a href="https://github.com/Picocrypt/Picocrypt/releases/download/1.47/Install-Picocrypt.exe">here</a> for easier launching. It does not require any admin permissions to install and it also bundles a software OpenGL renderer for compatibility, so if the portable executable isn't working, this installer likely will.
## macOS
Picocrypt for macOS is very simple as well. Download Picocrypt <a href="https://github.com/Picocrypt/Picocrypt/releases/latest/download/Picocrypt.dmg">here</a>, open the container, and drag Picocrypt to your Applications. You may need to manually trust the app from a terminal and control-click on the app if macOS prevents you from opening it:
```

View file

@ -1 +1 @@
1.47
1.48

View file

@ -11,50 +11,35 @@
<url type="homepage">https://github.com/Picocrypt/Picocrypt</url>
<url type="bugtracker">https://github.com/Picocrypt/Picocrypt/issues</url>
<content_rating type="oars-1.1"/>
<description>
<p>
Picocrypt is a very small (hence Pico), very simple, yet very secure encryption tool that you can use to protect your files. It is designed to be the go-to tool for encryption, with a focus on security, simplicity, and reliability. Picocrypt uses the secure XChaCha20 cipher and the Argon2id key derivation function to provide a high level of security, even from three-letter agencies like the NSA. Your privacy and security is under attack. Take it back with confidence by protecting your files with Picocrypt. For more info, please visit the project's GitHub repository.
</p>
<p>A list of features:</p>
<ul>
<li>Password generator: Picocrypt provides a secure password generator that you can use to create cryptographically secure passwords. You can customize the password length, as well as the types of characters to include.</li>
<li>Comments: Use this to store notes, information, and text along with the file (it won't be encrypted). For example, you can put a description of the file you're encrypting before sending it to someone. When the person you sent it to drops the file into Picocrypt, your description will be shown to that person. Comments are not authenticated, meaning it can be freely modified by an attacker. Thus, it should only be used for informational purposes in trusted environments.</li>
<li>Keyfiles: Picocrypt supports the use of keyfiles as an additional form of authentication (or the only form of authentication). It also provides a secure built-in keyfile generator.</li>
<li>Paranoid mode: Using this mode will encrypt your data with both XChaCha20 and Serpent in a cascade fashion, and use HMAC-SHA3 to authenticate data instead of BLAKE2b. Argon2 parameters will be increased significantly as well.</li>
<li>Reed-Solomon: This feature is very useful if you are planning to archive important data on a cloud provider or external medium for a long time. If checked, Picocrypt will use the Reed-Solomon error correction code to add 8 extra bytes for every 128 bytes of data to prevent file corruption. This means that up to ~3% of your file can corrupt and Picocrypt will still be able to correct the errors and decrypt your files with no corruption.</li>
<li>Force decrypt: Picocrypt automatically checks for file integrity upon decryption. If the file has been modified or is corrupted, Picocrypt will automatically delete the output for the user's safety. If you would like to override these safeguards, check this option. Also, if this option is checked and the Reed-Solomon feature was used on the encrypted volume, Picocrypt will attempt to recover as much of the file as possible during decryption.</li>
<li>Split into chunks: With Picocrypt, you can choose to split your output file into custom-sized chunks, so large files can become more manageable and easier to upload to cloud providers. Simply choose a unit (KiB, MiB, GiB, or TiB) and enter your desired chunk size for that unit. To decrypt the chunks, simply drag one of them into Picocrypt and the chunks will be automatically recombined during decryption.</li>
<li>Compress files: By default, Picocrypt uses a zip file with no compression to quickly merge files together when encrypting multiple files. If you would like to compress these files, however, simply check this box and the standard Deflate compression algorithm will be applied during encryption.</li>
<li>Deniability: Picocrypt volumes typically follow an easily recognizable header format. However, if you want to hide the fact that you are encrypting your files, enabling this option will provide you with plausible deniability. The output volume will indistinguishable from a stream of random bytes, and no one can prove it is a volume without the correct password.</li>
<li>Recursively: 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.</li>
</ul>
<p>Visit the project's GitHub repository to learn more.</p>
</description>
<categories>
<category>Utility</category>
<category>Security</category>
</categories>
<launchable type="desktop-id">io.github.picocrypt.Picocrypt.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://github.com/Picocrypt/Picocrypt/raw/4d4bd47efe88ff25f372db81c4249920d399226b/images/screenshot.png</image>
<image>https://raw.githubusercontent.com/Picocrypt/Picocrypt/refs/heads/main/images/screenshot.png</image>
<caption>Main window</caption>
</screenshot>
</screenshots>
<releases>
<release version="1.46" date="2025-01-29">
<url type="details">https://github.com/Picocrypt/Picocrypt/blob/main/Changelog.md#v146-released-01292025</url>
<release version="1.48" date="2025-04-18">
<url type="details">https://github.com/Picocrypt/Picocrypt/blob/main/Changelog.md#v148-released-04182025</url>
<description>
<ul>
<li>Added Picocrypt version to the window title.</li>
<li>Added ability to automatically unpack zip archives during decryption.</li>
<li>Allow pressing 'Enter' key to press Start/Process button</li>
<li>Update "Encrypt" button to "Zip and Encrypt" if multiple files</li>
<li>Give user estimated required free disk space in status label</li>
<li>Encrypt previously unencrypted temporary zip files</li>
<li>Add `.incomplete` to filenames while work is in progress</li>
<li>Use `encrypted-*.zip.pcv` output name instead of `Encrypted.zip.pcv`</li>
<li>Use 0700 permissions when auto unzipping and creating folders</li>
<li>Handle many more errors in the code where they were ignored previously</li>
</ul>
</description>
</release>
<release version="1.45" date="2024-12-05">
<description></description>
</release>
</releases>
</component>

View file

@ -1,6 +1,6 @@
1 VERSIONINFO
FILEVERSION 1,47,0,0
PRODUCTVERSION 1,47,0,0
FILEVERSION 1,48,0,0
PRODUCTVERSION 1,48,0,0
FILEOS 0x40004
FILETYPE 0x1
{
@ -8,7 +8,7 @@ BLOCK "StringFileInfo"
{
BLOCK "040904B0"
{
VALUE "FileVersion", "1.47"
VALUE "FileVersion", "1.48"
VALUE "LegalCopyright", "\xA9 Evan Su & contributors, GPLv3"
VALUE "ProductName", "Picocrypt"
}

View file

@ -2,7 +2,7 @@ package main
/*
Picocrypt v1.47
Picocrypt v1.48
Copyright (c) Evan Su
Released under a GNU GPL v3 License
https://github.com/Picocrypt/Picocrypt
@ -48,10 +48,11 @@ import (
)
// Constants
var KiB = 1 << 10
var MiB = 1 << 20
var GiB = 1 << 30
var TiB = 1 << 40
const KiB = 1 << 10
const MiB = 1 << 20
const GiB = 1 << 30
const 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}
@ -60,7 +61,7 @@ var TRANSPARENT = color.RGBA{0x00, 0x00, 0x00, 0x00}
// Generic variables
var window *giu.MasterWindow
var version = "v1.47"
var version = "v1.48"
var dpi float32
var mode string
var working bool
@ -130,6 +131,8 @@ var startLabel = "Start"
var mainStatus = "Ready"
var mainStatusColor = WHITE
var popupStatus string
var usingTempZip bool
var requiredFreeSpace int64
// Progress variables
var progress float32
@ -139,13 +142,13 @@ 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 rs1, rsErr1 = infectious.NewFEC(1, 3)
var rs5, rsErr2 = infectious.NewFEC(5, 15)
var rs16, rsErr3 = infectious.NewFEC(16, 48)
var rs24, rsErr4 = infectious.NewFEC(24, 72)
var rs32, rsErr5 = infectious.NewFEC(32, 96)
var rs64, rsErr6 = infectious.NewFEC(64, 192)
var rs128, rsErr7 = infectious.NewFEC(128, 136)
var fastDecode bool
// Compression variables and passthrough
@ -173,15 +176,156 @@ func (p *compressorProgress) Read(data []byte) (int, error) {
return read, err
}
type encryptedZipWriter struct {
_w io.Writer
_cipher *chacha20.Cipher
}
func (ezw *encryptedZipWriter) Write(data []byte) (n int, err error) {
dst := make([]byte, len(data))
ezw._cipher.XORKeyStream(dst, data)
return ezw._w.Write(dst)
}
type encryptedZipReader struct {
_r io.Reader
_cipher *chacha20.Cipher
}
func (ezr *encryptedZipReader) Read(data []byte) (n int, err error) {
src := make([]byte, len(data))
n, err = ezr._r.Read(src)
if err == nil && n > 0 {
dst := make([]byte, n)
ezr._cipher.XORKeyStream(dst, src[:n])
if copy(data, dst) != n {
panic(errors.New("built-in copy() function failed"))
}
}
return n, err
}
func onClickStartButton() {
// 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
giu.Update()
return
}
tmp, err := strconv.Atoi(splitSize)
if split && (splitSize == "" || err != nil || tmp <= 0) {
mainStatus = "Invalid chunk size"
mainStatusColor = RED
giu.Update()
return
}
// Check if output file already exists
_, err = os.Stat(outputFile)
// Check if any split chunks already exist
if split {
names, err2 := filepath.Glob(outputFile + ".*")
if err2 != nil {
panic(err2)
}
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
func draw() {
giu.SingleWindow().Flags(524351).Layout(
giu.Custom(func() {
if giu.IsKeyReleased(giu.KeyEnter) {
onClickStartButton()
return
}
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.SliderInt(&passgenLength, 12, 64).Size(giu.Auto),
),
giu.Checkbox("Uppercase", &passgenUpper),
giu.Checkbox("Lowercase", &passgenLower),
@ -282,7 +426,8 @@ func draw() {
}
if showProgress {
giu.PopupModal(" ##"+strconv.Itoa(modalId)).Flags(6).Layout(
giu.PopupModal("Progress:##"+strconv.Itoa(modalId)).Flags(6|1<<0).Layout(
giu.Dummy(0, 0),
giu.Row(
giu.ProgressBar(progress).Size(210, 0).Overlay(progressInfo),
giu.Style().SetDisabled(!canCancel).To(
@ -299,7 +444,7 @@ func draw() {
),
giu.Label(popupStatus),
).Build()
giu.OpenPopup(" ##" + strconv.Itoa(modalId))
giu.OpenPopup("Progress:##" + strconv.Itoa(modalId))
giu.Update()
}
}),
@ -442,25 +587,35 @@ func draw() {
}
return filepath.Dir(onlyFolders[0])
}())
f.SetInitFilename("Keyfile")
f.SetInitFilename("keyfile-" + strconv.Itoa(int(time.Now().Unix())) + ".bin")
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()
fout, err := os.Create(file)
if err != nil {
insufficientSpace(nil, nil)
os.Remove(file)
mainStatus = "Failed to create keyfile"
mainStatusColor = RED
giu.Update()
return
}
data := make([]byte, 32)
if n, err := rand.Read(data); err != nil || n != 32 {
panic(errors.New("fatal crypto/rand error"))
}
n, err := fout.Write(data)
if n != 32 {
fout.Close()
panic(errors.New("failed to write full keyfile"))
}
if err := fout.Close(); err != nil {
panic(err)
} else {
mainStatus = "Ready"
mainStatusColor = WHITE
giu.Update()
return
}
}),
giu.Tooltip("Generate a cryptographically secure keyfile"),
@ -499,16 +654,8 @@ func draw() {
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.Style().SetDisabled(recursively || !(len(allFiles) > 1 || len(onlyFolders) > 0)).To(
giu.Checkbox("Compress files", &compress),
giu.Tooltip("Compress files with Deflate before encrypting"),
),
).Build()
@ -523,13 +670,13 @@ func draw() {
giu.Row(
giu.Checkbox("Deniability", &deniability),
giu.Tooltip("Add plausible deniability to the volume\nIf enabled, comments will not be usable"),
giu.Tooltip("Warning: only use this if you know what it does!"),
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"),
giu.Tooltip("Warning: only use this if you know what it does!"),
),
).Build()
@ -562,7 +709,7 @@ func draw() {
sameLevel = false
}
}),
giu.Tooltip("Extract .zip upon decryption (may overwrite)"),
giu.Tooltip("Extract .zip upon decryption (may overwrite files)"),
),
giu.Dummy(-170, 0),
giu.Style().SetDisabled(!autoUnzip).To(
@ -612,7 +759,7 @@ func draw() {
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")
f.SetInitFilename("encrypted-" + strconv.Itoa(int(time.Now().Unix())))
}
// Get the chosen file path
@ -653,106 +800,40 @@ func draw() {
return startLabel
}
return "Process"
}()).Size(giu.Auto, 34).OnClick(func() {
if keyfile && keyfiles == nil {
mainStatus = "Please select your keyfiles"
mainStatusColor = RED
}()).Size(giu.Auto, 34).OnClick(onClickStartButton),
giu.Custom(func() {
if mainStatus != "Ready" {
giu.Style().SetColor(giu.StyleColorText, mainStatusColor).To(
giu.Label(mainStatus),
).Build()
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 requiredFreeSpace > 0 {
multiplier := 1
if len(allFiles) > 1 || len(onlyFolders) > 0 { // need a temporary zip file
multiplier++
}
}
// 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()
}()
if deniability {
multiplier++
}
if split {
multiplier++
}
if recombine {
multiplier++
}
if autoUnzip {
multiplier++
}
giu.Style().SetColor(giu.StyleColorText, WHITE).To(
giu.Label("Ready (ensure >" + sizeify(requiredFreeSpace*int64(multiplier)) + " of disk space is free)"),
).Build()
} else {
giu.Style().SetColor(giu.StyleColorText, WHITE).To(
giu.Label("Ready"),
).Build()
}
}),
giu.Style().SetColor(giu.StyleColorText, mainStatusColor).To(
giu.Label(mainStatus),
),
),
giu.Custom(func() {
@ -812,19 +893,25 @@ func onDrop(names []string) {
// One item dropped
if len(names) == 1 {
stat, _ := os.Stat(names[0])
stat, err := os.Stat(names[0])
if err != nil {
mainStatus = "Failed to stat dropped item"
mainStatusColor = RED
return
}
// A folder was dropped
if stat.IsDir() {
folders++
mode = "encrypt"
inputLabel = "1 folder"
startLabel = "Encrypt"
startLabel = "Zip and Encrypt"
onlyFolders = append(onlyFolders, names[0])
inputFile = filepath.Join(filepath.Dir(names[0]), "Encrypted") + ".zip"
inputFile = filepath.Join(filepath.Dir(names[0]), "encrypted-"+strconv.Itoa(int(time.Now().Unix()))) + ".zip"
outputFile = inputFile + ".pcv"
} else { // A file was dropped
files++
requiredFreeSpace = stat.Size()
// Is the file a part of a split volume?
nums := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}
@ -862,6 +949,7 @@ func onDrop(names []string) {
totalFiles++
compressTotal += stat.Size()
}
requiredFreeSpace = compressTotal
} else {
outputFile = names[0][:len(names[0])-4]
}
@ -950,7 +1038,7 @@ func onDrop(names []string) {
}
} else { // There are multiple dropped items
mode = "encrypt"
startLabel = "Encrypt"
startLabel = "Zip and Encrypt"
// Go through each dropped item and add to corresponding slices
for _, name := range names {
@ -964,6 +1052,7 @@ func onDrop(names []string) {
allFiles = append(allFiles, name)
compressTotal += stat.Size()
requiredFreeSpace += stat.Size()
inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal))
giu.Update()
}
@ -987,8 +1076,9 @@ func onDrop(names []string) {
}
// Set the input and output paths
inputFile = filepath.Join(filepath.Dir(names[0]), "Encrypted") + ".zip"
inputFile = filepath.Join(filepath.Dir(names[0]), "encrypted-"+strconv.Itoa(int(time.Now().Unix()))) + ".zip"
outputFile = inputFile + ".pcv"
usingTempZip = true
}
// Recursively add all files in 'onlyFolders' to 'allFiles'
@ -1001,6 +1091,7 @@ func onDrop(names []string) {
if err == nil && !stat.IsDir() {
allFiles = append(allFiles, path)
compressTotal += stat.Size()
requiredFreeSpace += stat.Size()
inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal))
giu.Update()
}
@ -1033,8 +1124,19 @@ func work() {
var keyfileHashRef []byte // Same as 'keyfileHash', but used for comparison
var authTag []byte // 64-byte authentication tag (BLAKE2b or HMAC-SHA3)
var tempZipCipherW *chacha20.Cipher
var tempZipCipherR *chacha20.Cipher
var tempZipInUse bool = false
func() {
key, nonce := make([]byte, 32), make([]byte, 12)
rand.Read(key)
rand.Read(nonce)
tempZipCipherW, _ = chacha20.NewUnauthenticatedCipher(key, nonce)
tempZipCipherR, _ = chacha20.NewUnauthenticatedCipher(key, nonce)
}()
// Combine/compress all files into a .zip file if needed
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
if len(allFiles) > 1 || len(onlyFolders) > 0 {
// Consider case where compressing only one file
files := allFiles
if len(allFiles) == 0 {
@ -1050,7 +1152,7 @@ func work() {
}
// Open a temporary .zip for writing
inputFile = strings.TrimSuffix(outputFile, ".pcv")
inputFile = strings.TrimSuffix(outputFile, ".pcv") + ".tmp"
file, err := os.Create(inputFile)
if err != nil { // Make sure file is writable
accessDenied("Write")
@ -1058,7 +1160,12 @@ func work() {
}
// Add each file to the .zip
writer := zip.NewWriter(file)
tempZip := encryptedZipWriter{
_w: file,
_cipher: tempZipCipherW,
}
tempZipInUse = true
writer := zip.NewWriter(&tempZip)
compressStart = time.Now()
for i, path := range files {
progressInfo = fmt.Sprintf("%d/%d", i+1, len(files))
@ -1318,7 +1425,7 @@ func work() {
}
// Create the output file
fout, err = os.Create(outputFile)
fout, err = os.Create(outputFile + ".incomplete")
if err != nil {
fin.Close()
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
@ -1403,7 +1510,7 @@ func work() {
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
os.Remove(outputFile)
os.Remove(fout.Name())
return
}
}
@ -1635,7 +1742,7 @@ func work() {
}
// Create the output file for decryption
fout, err = os.Create(outputFile)
fout, err = os.Create(outputFile + ".incomplete")
if err != nil {
fin.Close()
if recombine {
@ -1691,13 +1798,17 @@ func work() {
// Start the main encryption process
canCancel = true
startTime := time.Now()
tempZip := encryptedZipReader{
_r: fin,
_cipher: tempZipCipherR,
}
for {
if !working {
cancel(fin, fout)
if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
os.Remove(outputFile)
os.Remove(fout.Name())
return
}
@ -1708,7 +1819,13 @@ func work() {
} else {
src = make([]byte, MiB)
}
size, err := fin.Read(src)
var size int
if tempZipInUse {
size, err = tempZip.Read(src)
} else {
size, err = fin.Read(src)
}
if err != nil {
break
}
@ -1828,7 +1945,7 @@ func work() {
if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile)
}
os.Remove(outputFile)
os.Remove(fout.Name())
return
}
@ -1907,6 +2024,8 @@ func work() {
fin.Close()
fout.Close()
os.Rename(outputFile+".incomplete", outputFile)
// Add plausible deniability
if mode == "encrypt" && deniability {
popupStatus = "Adding plausible deniability..."
@ -1914,13 +2033,13 @@ func work() {
giu.Update()
// Get size of volume for showing progress
stat, _ := os.Stat(fout.Name())
stat, _ := os.Stat(outputFile)
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())
os.Rename(outputFile, outputFile+".tmp")
fin, _ := os.Open(outputFile + ".tmp")
fout, _ := os.Create(outputFile + ".incomplete")
// Use a random Argon2 salt and XChaCha20 nonce
salt := make([]byte, 16)
@ -1970,6 +2089,7 @@ func work() {
fin.Close()
fout.Close()
os.Remove(fin.Name())
os.Rename(outputFile+".incomplete", outputFile)
canCancel = true
giu.Update()
}
@ -2014,7 +2134,7 @@ func work() {
startTime := time.Now()
for i := 0; i < chunks; i++ {
// Make the chunk
fout, _ := os.Create(fmt.Sprintf("%s.%d", outputFile, i))
fout, _ := os.Create(fmt.Sprintf("%s.%d.incomplete", outputFile, i))
done := 0
// Copy data into the chunk
@ -2080,6 +2200,10 @@ func work() {
fin.Close()
os.Remove(outputFile)
names, _ = filepath.Glob(outputFile + ".*.incomplete")
for _, i := range names {
os.Rename(i, strings.TrimSuffix(i, ".incomplete"))
}
}
canCancel = false
@ -2251,6 +2375,8 @@ func resetUI() {
mainStatus = "Ready"
mainStatusColor = WHITE
popupStatus = ""
usingTempZip = false
requiredFreeSpace = 0
progress = 0
progressInfo = ""
@ -2395,7 +2521,7 @@ func unpackArchive(zipPath string) error {
// Make directory if current entry is a folder
if f.FileInfo().IsDir() {
if err := os.MkdirAll(outPath, f.Mode()); err != nil {
if err := os.MkdirAll(outPath, 0700); err != nil {
return err
}
}
@ -2414,7 +2540,7 @@ func unpackArchive(zipPath string) error {
outPath := filepath.Join(extractDir, f.Name)
// Otherwise create necessary parent directories
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
if err := os.MkdirAll(filepath.Dir(outPath), 0700); err != nil {
return err
}
@ -2463,6 +2589,9 @@ func unpackArchive(zipPath string) error {
}
func main() {
if rsErr1 != nil || rsErr2 != nil || rsErr3 != nil || rsErr4 != nil || rsErr5 != nil || rsErr6 != nil || rsErr7 != nil {
panic(errors.New("rs failed to init"))
}
// Create the main window
window = giu.NewMasterWindow("Picocrypt "+version[1:], 318, 507, giu.MasterWindowFlagsNotResizable)

View file

@ -3,18 +3,18 @@ module Picocrypt
go 1.24.2
require (
github.com/Picocrypt/dialog v0.0.0-20240831001746-9ca708a9cd29
github.com/Picocrypt/giu v0.0.0-20240831005244-5771b35043ac
github.com/Picocrypt/imgui-go v0.0.0-20240831004007-6f60d7beadf6
github.com/Picocrypt/infectious v0.0.0-20240830233326-3a050f65f9ec
github.com/Picocrypt/dialog v0.0.0-20250412233924-78f7b909315b
github.com/Picocrypt/giu v0.0.0-20250412235908-fe90a482e6f2
github.com/Picocrypt/imgui-go v0.0.0-20250412235405-d86b230f5fbb
github.com/Picocrypt/infectious v0.0.0-20250412183341-9f88c6307b39
github.com/Picocrypt/serpent v0.0.0-20240830233833-9ad6ab254fd7
github.com/Picocrypt/zxcvbn-go v0.0.0-20240831000415-fccb38ccb913
github.com/Picocrypt/zxcvbn-go v0.0.0-20250412183938-d59695960527
golang.org/x/crypto v0.37.0
)
require (
github.com/Picocrypt/gl v0.0.0-20240831002619-6531d2bba5fc // indirect
github.com/Picocrypt/glfw/v3.3/glfw v0.0.0-20240831003212-7f16c5fb374b // indirect
github.com/Picocrypt/gl v0.0.0-20250412234430-767b58dbf936 // indirect
github.com/Picocrypt/glfw/v3.3/glfw v0.0.0-20250412234750-7b96bfdb8dd8 // indirect
github.com/Picocrypt/mainthread v0.0.0-20240831004314-496f638392b3 // indirect
github.com/Picocrypt/w32 v0.0.0-20240831001500-1183079d4d57 // indirect
golang.org/x/sys v0.32.0 // indirect

View file

@ -1,23 +1,23 @@
github.com/Picocrypt/dialog v0.0.0-20240831001746-9ca708a9cd29 h1:WIgRST/mpLiBEG2MF5MRPBDYYevLw7y14cwUEDjG5+Q=
github.com/Picocrypt/dialog v0.0.0-20240831001746-9ca708a9cd29/go.mod h1:raXVkdcX4495+fW9Ac+kvPMHRNk0rOcNXEWFD71B2As=
github.com/Picocrypt/giu v0.0.0-20240831005244-5771b35043ac h1:Z21enGbi450NyI7UZSoEuu//axifyGl63BJVjHX3ZXc=
github.com/Picocrypt/giu v0.0.0-20240831005244-5771b35043ac/go.mod h1:x7jbmZVofU9rn5WJj2+riU85Zo0MFlfp1sMTnKFQhc0=
github.com/Picocrypt/gl v0.0.0-20240831002619-6531d2bba5fc h1:5ckKMFhiz/Af6+sdkGlw74BU+rKRmoFWqU/rXHGUe3g=
github.com/Picocrypt/gl v0.0.0-20240831002619-6531d2bba5fc/go.mod h1:VknKAZzEoKP9nqrc/dveCwR5L01B9V8yLqtpvYmQ3DA=
github.com/Picocrypt/glfw/v3.3/glfw v0.0.0-20240831003212-7f16c5fb374b h1:hSaQU4P9KbMg9s2Jp2mTk9G5G+zkf4Yse5YRoxWTDTk=
github.com/Picocrypt/glfw/v3.3/glfw v0.0.0-20240831003212-7f16c5fb374b/go.mod h1:r5awTCSm/ugmTRKmT8Hr0T4xGPI6K35eFK0s3jYCW+s=
github.com/Picocrypt/imgui-go v0.0.0-20240831004007-6f60d7beadf6 h1:Y6SuxbSkQSU1hdEOpoMvp6Akq3RVX6KP1U4pKjGv3qo=
github.com/Picocrypt/imgui-go v0.0.0-20240831004007-6f60d7beadf6/go.mod h1:mGfOCkgyafVMIs1tU70va3lFSh6hSb+Vq4paVLX1Fjg=
github.com/Picocrypt/infectious v0.0.0-20240830233326-3a050f65f9ec h1:/cop0/v0HxIJm1XGDgIlzNJ3e4HhM8nIUPZi5RZ/n1w=
github.com/Picocrypt/infectious v0.0.0-20240830233326-3a050f65f9ec/go.mod h1:aaFq/WMVxrU2Exl/tXbTFSXajZrqw0mgn/wi42n0fK4=
github.com/Picocrypt/dialog v0.0.0-20250412233924-78f7b909315b h1:k5YGEx61N6K8l2l6AQ1u5W2aR+47sVZWFyqXS/f5lIA=
github.com/Picocrypt/dialog v0.0.0-20250412233924-78f7b909315b/go.mod h1:OyaP0Tz19qL3RAGq5Ntues+WVrIbHh5MrfqoA/qhqeg=
github.com/Picocrypt/giu v0.0.0-20250412235908-fe90a482e6f2 h1:SPR2efZTpZJON/2mNifLi68Gl9Epxh/1nXb3kGGHCcg=
github.com/Picocrypt/giu v0.0.0-20250412235908-fe90a482e6f2/go.mod h1:jd6AonK0ZI02R7GqLWb4gWJz/A2ClF36Y4fFMR8Lzbk=
github.com/Picocrypt/gl v0.0.0-20250412234430-767b58dbf936 h1:6MChjQ4AZC2ISBjbgZU/z6tSUxYP50NkRvAu0T2kjlY=
github.com/Picocrypt/gl v0.0.0-20250412234430-767b58dbf936/go.mod h1:pMdf3io/y3I+zYZ6/xFb3MlI2AgL38enDDIKuR0n2qA=
github.com/Picocrypt/glfw/v3.3/glfw v0.0.0-20250412234750-7b96bfdb8dd8 h1:i8wXJhSYIJTXb6sqBS6JZW7QosI9u8Ysy1BHZCTuZEc=
github.com/Picocrypt/glfw/v3.3/glfw v0.0.0-20250412234750-7b96bfdb8dd8/go.mod h1:cX5N2TrX03DC5i5eplxopglDue/vHDs+6Ng9G9uItaI=
github.com/Picocrypt/imgui-go v0.0.0-20250412235405-d86b230f5fbb h1:0XMtv2CXx3QvC9ikeH43fJl6ql8j5EsnaiOqhsToFnY=
github.com/Picocrypt/imgui-go v0.0.0-20250412235405-d86b230f5fbb/go.mod h1:N+NVTIIMz6icYltvaKHMvmVIllZDYUyscJ8wpcLKDZ4=
github.com/Picocrypt/infectious v0.0.0-20250412183341-9f88c6307b39 h1:czHyPoiNrILv9xjfQ87UFllJgak8W6gVcYkmfOay/BE=
github.com/Picocrypt/infectious v0.0.0-20250412183341-9f88c6307b39/go.mod h1:2ZVEanURxuWmxYZ6W6xMMy4ZR6xmQr16Vq/XPTLIspQ=
github.com/Picocrypt/mainthread v0.0.0-20240831004314-496f638392b3 h1:a62XmbZYhHGDR15C1gxp/IPfJX5SflrJuGpqNoOOK7w=
github.com/Picocrypt/mainthread v0.0.0-20240831004314-496f638392b3/go.mod h1:bsUKeX+/53rCTrItl3YUaeaN5tXl1v6326ZI90xIOsc=
github.com/Picocrypt/serpent v0.0.0-20240830233833-9ad6ab254fd7 h1:G36G2vmQAS7CVoHQrHDGAoCWll/0kPCI8Dk7mgwcJFE=
github.com/Picocrypt/serpent v0.0.0-20240830233833-9ad6ab254fd7/go.mod h1:BxsgRYwUVd92aEwXnXsfXfHw8aHlD/PUyExC/wwk9oI=
github.com/Picocrypt/w32 v0.0.0-20240831001500-1183079d4d57 h1:jusSXTp0h5wz8lxNXStw0jXr/ogZF6rzRF8gu0534hA=
github.com/Picocrypt/w32 v0.0.0-20240831001500-1183079d4d57/go.mod h1:FkeZHdKlITdP34VknO8yLdRY5pCi+iWEhDSA0YsBhZc=
github.com/Picocrypt/zxcvbn-go v0.0.0-20240831000415-fccb38ccb913 h1:QGv9QiTkNZ2iRmXEd7nNopaUJMBhBdBcsvWPl+v51AY=
github.com/Picocrypt/zxcvbn-go v0.0.0-20240831000415-fccb38ccb913/go.mod h1:dMyJ/0E4MeBo2wH1ZYmvPTChnYSj2MjLUndvYQt0vGw=
github.com/Picocrypt/zxcvbn-go v0.0.0-20250412183938-d59695960527 h1:IqypAzv5COsByMhiSdwlgafA5SBRG7Z0binnBSo3htM=
github.com/Picocrypt/zxcvbn-go v0.0.0-20250412183938-d59695960527/go.mod h1:u0rcUNEwy7st1DnPxdOJdTsh0aSRhrdMOxlIGrXR1Ls=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=