Compare commits

...

13 commits

Author SHA1 Message Date
Evan Su
504e908195
Merge 9b2b69e442 into f0bfe3ba03 2025-04-14 00:06:28 +00:00
Evan Su
9b2b69e442 Update versioninfo.rc
Some checks failed
CodeQL / Analyze (push) Has been cancelled
2025-04-13 20:06:21 -04:00
Evan Su
f58f5ce249 Create close-issues.yml 2025-04-13 19:59:07 -04:00
Evan Su
b26137d959 Update default.yml 2025-04-13 19:51:27 -04:00
Evan Su
065a50d90e fix: 0700 instead of 0600 for mkdirall 2025-04-13 18:54:00 -04:00
Evan Su
c63cf92672 Start button show "Zip and Encrypt" if temp zip needed 2025-04-13 15:15:43 -04:00
Evan Su
6a8fdeaa53 Remove caveats from README
No longer needed because temporary zip files are now encrypted.
2025-04-13 14:47:18 -04:00
Evan Su
bad71f95ce use 0600 for auto unzip file permissions
prevent executing for safety and only allow user to have access
2025-04-13 14:40:52 -04:00
Evan Su
d7a0ee126b Use encrypted-*.zip.pcv instead of Encrypted.zip.pcv
So you don't always have to rename or delete an existing volume
2025-04-13 14:33:08 -04:00
Evan Su
16bb70dc97 Add .incomplete to end of wip files 2025-04-13 14:24:02 -04:00
Evan Su
9e7e2e9c44 Changelog: add to future: remove use of temp files 2025-04-13 13:35:01 -04:00
Evan Su
757c9c23e4 remove warning for external storage target
since temporary files are now encrypted so no longer matters
2025-04-13 13:32:56 -04:00
Evan Su
159944a619 Encrypt temporary zip files 2025-04-13 13:23:19 -04:00
6 changed files with 163 additions and 84 deletions

View file

@ -12,11 +12,11 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: markdown
attributes: attributes:
value: | 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 - type: markdown
attributes: attributes:
value: | value: |
@ -24,7 +24,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: markdown
attributes: attributes:
value: | value: |
@ -32,7 +32,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: markdown
attributes: attributes:
value: | value: |
@ -40,7 +40,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: markdown
attributes: attributes:
value: | value: |
@ -48,7 +48,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: markdown
attributes: attributes:
value: | value: |
@ -56,7 +56,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: markdown
attributes: attributes:
value: | value: |
@ -64,7 +64,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: markdown
attributes: attributes:
value: | value: |
@ -72,8 +72,9 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: checkboxes
id: confirmation
attributes: attributes:
label: "Please confirm:" label: "Please confirm:"
options: options:
@ -83,7 +84,12 @@ body:
required: true required: true
- label: "I acknowledge my issue may be ignored or closed without explanation" - label: "I acknowledge my issue may be ignored or closed without explanation"
required: true 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 - type: input
id: summary
attributes: attributes:
label: "Describe the issue briefly in a few sentences:" label: "Describe the issue briefly in a few sentences:"
description: "You can add more details in a separate comment after creating the issue." 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 }}

View file

@ -1,7 +1,17 @@
# v1.48 (Released 04/11/2025) # Future
<ul>
<li>Figure out how to remove use of temporary files completely</li>
</ul>
# v1.48 (Released 04/15/2025)
<ul> <ul>
<li>✓ Allow pressing 'Enter' key to press Start/Process button</li> <li>✓ Allow pressing 'Enter' key to press Start/Process button</li>
<li>✓ Warn user when encrypting multiple files to an external drive</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>
</ul> </ul>
# v1.47 (Released 02/19/2025) # v1.47 (Released 02/19/2025)

View file

@ -90,14 +90,6 @@ 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, 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. I've also meticulously gone through every single setting in the Picocrypt organization and repos, locking down access behind multiple layers of security such as read-only base-level member permissions, required PRs and mandatory approvals (which no one can do but me), mandatory CODEOWNERS approvals, and I'm the only member of the Picocrypt organization and repos (except for PicoGo). 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, 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. I've also meticulously gone through every single setting in the Picocrypt organization and repos, locking down access behind multiple layers of security such as read-only base-level member permissions, required PRs and mandatory approvals (which no one can do but me), mandatory CODEOWNERS approvals, and I'm the only member of the Picocrypt organization and repos (except for PicoGo). You can feel confident about using Picocrypt as long as you understand:

View file

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

View file

@ -30,7 +30,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -131,9 +130,8 @@ var startLabel = "Start"
var mainStatus = "Ready" var mainStatus = "Ready"
var mainStatusColor = WHITE var mainStatusColor = WHITE
var popupStatus string var popupStatus string
var usingTempZip bool
var temporaryZip bool var requiredFreeSpace int64
var externalDst bool
// Progress variables // Progress variables
var progress float32 var progress float32
@ -177,6 +175,33 @@ func (p *compressorProgress) Read(data []byte) (int, error) {
return read, err 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])
copy(data, dst)
}
return n, err
}
var onClickStartButton = func() { var onClickStartButton = func() {
// Start button should be disabled if these conditions are true; don't do anything if so // 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) { if (len(keyfiles) == 0 && password == "") || (mode == "encrypt" && password != cpassword) {
@ -611,15 +636,7 @@ func draw() {
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 || !(len(allFiles) > 1 || len(onlyFolders) > 0)).To( giu.Style().SetDisabled(recursively || !(len(allFiles) > 1 || len(onlyFolders) > 0)).To(
giu.Checkbox("Compress files", &compress).OnChange(func() { giu.Checkbox("Compress files", &compress),
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() ).Build()
@ -723,7 +740,7 @@ func draw() {
tmp := strings.TrimSuffix(filepath.Base(outputFile), ".pcv") tmp := strings.TrimSuffix(filepath.Base(outputFile), ".pcv")
f.SetInitFilename(strings.TrimSuffix(tmp, filepath.Ext(tmp))) f.SetInitFilename(strings.TrimSuffix(tmp, filepath.Ext(tmp)))
if mode == "encrypt" && (len(allFiles) > 1 || len(onlyFolders) > 0 || compress) { 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 // Get the chosen file path
@ -740,21 +757,6 @@ 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"
@ -787,20 +789,12 @@ func draw() {
).Build() ).Build()
return return
} }
if temporaryZip && externalDst { if requiredFreeSpace > 0 {
giu.Style().SetColor(giu.StyleColorText, YELLOW).To(
giu.Label("Warning: unencrypted temp files will be created"),
).Build()
} else if temporaryZip {
giu.Style().SetColor(giu.StyleColorText, WHITE).To( giu.Style().SetColor(giu.StyleColorText, WHITE).To(
giu.Label("Ready (info: will create a temporary zip file)"), giu.Label("Ready (ensure " + sizeify(requiredFreeSpace) + " of disk space is free)"),
).Build()
} else if externalDst {
giu.Style().SetColor(giu.StyleColorText, WHITE).To(
giu.Label("Ready (info: target may be an external drive)"),
).Build() ).Build()
} else { } else {
giu.Style().SetColor(giu.StyleColorText, mainStatusColor).To( giu.Style().SetColor(giu.StyleColorText, WHITE).To(
giu.Label("Ready"), giu.Label("Ready"),
).Build() ).Build()
} }
@ -871,12 +865,18 @@ func onDrop(names []string) {
folders++ folders++
mode = "encrypt" mode = "encrypt"
inputLabel = "1 folder" inputLabel = "1 folder"
startLabel = "Encrypt" startLabel = "Zip and Encrypt"
onlyFolders = append(onlyFolders, names[0]) 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" outputFile = inputFile + ".pcv"
size, err := dirSize(names[0])
if err != nil {
panic(err)
}
requiredFreeSpace = 2 * size
} else { // A file was dropped } else { // A file was dropped
files++ files++
requiredFreeSpace += stat.Size()
// Is the file a part of a split volume? // Is the file a part of a split volume?
nums := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} nums := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}
@ -913,6 +913,7 @@ func onDrop(names []string) {
} }
totalFiles++ totalFiles++
compressTotal += stat.Size() compressTotal += stat.Size()
requiredFreeSpace += stat.Size()
} }
} else { } else {
outputFile = names[0][:len(names[0])-4] outputFile = names[0][:len(names[0])-4]
@ -1002,7 +1003,7 @@ func onDrop(names []string) {
} }
} else { // There are multiple dropped items } else { // There are multiple dropped items
mode = "encrypt" mode = "encrypt"
startLabel = "Encrypt" startLabel = "Zip and Encrypt"
// Go through each dropped item and add to corresponding slices // Go through each dropped item and add to corresponding slices
for _, name := range names { for _, name := range names {
@ -1016,6 +1017,7 @@ func onDrop(names []string) {
allFiles = append(allFiles, name) allFiles = append(allFiles, name)
compressTotal += stat.Size() compressTotal += stat.Size()
requiredFreeSpace += 2 * stat.Size()
inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal)) inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal))
giu.Update() giu.Update()
} }
@ -1039,9 +1041,9 @@ 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-"+strconv.Itoa(int(time.Now().Unix()))) + ".zip"
outputFile = inputFile + ".pcv" outputFile = inputFile + ".pcv"
temporaryZip = true usingTempZip = true
} }
// Recursively add all files in 'onlyFolders' to 'allFiles' // Recursively add all files in 'onlyFolders' to 'allFiles'
@ -1054,6 +1056,7 @@ func onDrop(names []string) {
if err == nil && !stat.IsDir() { if err == nil && !stat.IsDir() {
allFiles = append(allFiles, path) allFiles = append(allFiles, path)
compressTotal += stat.Size() compressTotal += stat.Size()
requiredFreeSpace += 2 * stat.Size()
inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal)) inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal))
giu.Update() giu.Update()
} }
@ -1086,6 +1089,17 @@ func work() {
var keyfileHashRef []byte // Same as 'keyfileHash', but used for comparison var keyfileHashRef []byte // Same as 'keyfileHash', but used for comparison
var authTag []byte // 64-byte authentication tag (BLAKE2b or HMAC-SHA3) 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 // 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 || compress {
// Consider case where compressing only one file // Consider case where compressing only one file
@ -1103,7 +1117,7 @@ func work() {
} }
// Open a temporary .zip for writing // Open a temporary .zip for writing
inputFile = strings.TrimSuffix(outputFile, ".pcv") inputFile = strings.TrimSuffix(outputFile, ".pcv") + ".tmp"
file, err := os.Create(inputFile) file, err := os.Create(inputFile)
if err != nil { // Make sure file is writable if err != nil { // Make sure file is writable
accessDenied("Write") accessDenied("Write")
@ -1111,7 +1125,12 @@ func work() {
} }
// Add each file to the .zip // 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() compressStart = time.Now()
for i, path := range files { for i, path := range files {
progressInfo = fmt.Sprintf("%d/%d", i+1, len(files)) progressInfo = fmt.Sprintf("%d/%d", i+1, len(files))
@ -1371,7 +1390,7 @@ func work() {
} }
// Create the output file // Create the output file
fout, err = os.Create(outputFile) fout, err = os.Create(outputFile + ".incomplete")
if err != nil { if err != nil {
fin.Close() fin.Close()
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress { if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
@ -1456,7 +1475,7 @@ func work() {
if len(allFiles) > 1 || len(onlyFolders) > 0 || compress { if len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile) os.Remove(inputFile)
} }
os.Remove(outputFile) os.Remove(fout.Name())
return return
} }
} }
@ -1688,7 +1707,7 @@ func work() {
} }
// Create the output file for decryption // Create the output file for decryption
fout, err = os.Create(outputFile) fout, err = os.Create(outputFile + ".incomplete")
if err != nil { if err != nil {
fin.Close() fin.Close()
if recombine { if recombine {
@ -1744,13 +1763,17 @@ func work() {
// Start the main encryption process // Start the main encryption process
canCancel = true canCancel = true
startTime := time.Now() startTime := time.Now()
tempZip := encryptedZipReader{
_r: fin,
_cipher: tempZipCipherR,
}
for { for {
if !working { if !working {
cancel(fin, fout) cancel(fin, fout)
if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress { if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile) os.Remove(inputFile)
} }
os.Remove(outputFile) os.Remove(fout.Name())
return return
} }
@ -1761,7 +1784,13 @@ func work() {
} else { } else {
src = make([]byte, MiB) 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 { if err != nil {
break break
} }
@ -1881,7 +1910,7 @@ func work() {
if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress { if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress {
os.Remove(inputFile) os.Remove(inputFile)
} }
os.Remove(outputFile) os.Remove(fout.Name())
return return
} }
@ -1960,6 +1989,8 @@ func work() {
fin.Close() fin.Close()
fout.Close() fout.Close()
os.Rename(outputFile+".incomplete", outputFile)
// Add plausible deniability // Add plausible deniability
if mode == "encrypt" && deniability { if mode == "encrypt" && deniability {
popupStatus = "Adding plausible deniability..." popupStatus = "Adding plausible deniability..."
@ -1967,13 +1998,13 @@ func work() {
giu.Update() giu.Update()
// Get size of volume for showing progress // Get size of volume for showing progress
stat, _ := os.Stat(fout.Name()) stat, _ := os.Stat(outputFile)
total := stat.Size() total := stat.Size()
// Rename the output volume to free up the filename // Rename the output volume to free up the filename
os.Rename(fout.Name(), fout.Name()+".tmp") os.Rename(outputFile, outputFile+".tmp")
fin, _ := os.Open(fout.Name() + ".tmp") fin, _ := os.Open(outputFile + ".tmp")
fout, _ := os.Create(fout.Name()) fout, _ := os.Create(outputFile + ".incomplete")
// Use a random Argon2 salt and XChaCha20 nonce // Use a random Argon2 salt and XChaCha20 nonce
salt := make([]byte, 16) salt := make([]byte, 16)
@ -2023,6 +2054,7 @@ func work() {
fin.Close() fin.Close()
fout.Close() fout.Close()
os.Remove(fin.Name()) os.Remove(fin.Name())
os.Rename(outputFile+".incomplete", outputFile)
canCancel = true canCancel = true
giu.Update() giu.Update()
} }
@ -2067,7 +2099,7 @@ func work() {
startTime := time.Now() startTime := time.Now()
for i := 0; i < chunks; i++ { for i := 0; i < chunks; i++ {
// Make the chunk // Make the chunk
fout, _ := os.Create(fmt.Sprintf("%s.%d", outputFile, i)) fout, _ := os.Create(fmt.Sprintf("%s.%d.incomplete", outputFile, i))
done := 0 done := 0
// Copy data into the chunk // Copy data into the chunk
@ -2133,6 +2165,10 @@ func work() {
fin.Close() fin.Close()
os.Remove(outputFile) os.Remove(outputFile)
names, _ = filepath.Glob(outputFile + ".*.incomplete")
for _, i := range names {
os.Rename(i, strings.TrimSuffix(i, ".incomplete"))
}
} }
canCancel = false canCancel = false
@ -2304,8 +2340,8 @@ func resetUI() {
mainStatus = "Ready" mainStatus = "Ready"
mainStatusColor = WHITE mainStatusColor = WHITE
popupStatus = "" popupStatus = ""
temporaryZip = false usingTempZip = false
externalDst = false requiredFreeSpace = 0
progress = 0 progress = 0
progressInfo = "" progressInfo = ""
@ -2450,7 +2486,7 @@ func unpackArchive(zipPath string) error {
// Make directory if current entry is a folder // Make directory if current entry is a folder
if f.FileInfo().IsDir() { if f.FileInfo().IsDir() {
if err := os.MkdirAll(outPath, f.Mode()); err != nil { if err := os.MkdirAll(outPath, 0700); err != nil {
return err return err
} }
} }
@ -2469,7 +2505,7 @@ func unpackArchive(zipPath string) error {
outPath := filepath.Join(extractDir, f.Name) outPath := filepath.Join(extractDir, f.Name)
// Otherwise create necessary parent directories // 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 return err
} }
@ -2517,6 +2553,20 @@ func unpackArchive(zipPath string) error {
return nil return nil
} }
func dirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return err
})
return size, err
}
func main() { func main() {
// Create the main window // Create the main window
window = giu.NewMasterWindow("Picocrypt "+version[1:], 318, 507, giu.MasterWindowFlagsNotResizable) window = giu.NewMasterWindow("Picocrypt "+version[1:], 318, 507, giu.MasterWindowFlagsNotResizable)