diff --git a/.github/ISSUE_TEMPLATE/default.yml b/.github/ISSUE_TEMPLATE/default.yml index 9aa09e5..81bad47 100644 --- a/.github/ISSUE_TEMPLATE/default.yml +++ b/.github/ISSUE_TEMPLATE/default.yml @@ -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,15 @@ 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: | + ### Picocrypt is crashing + - type: markdown + attributes: + value: | + This is almost always caused by input/output files being in locations where you don't have the correct read/write permissions. Try working within your user/home folder only and copy to/from other places to see if that resolves the crash. If not, run Picocrypt from the command line (e.g. `Picocrypt.exe` or `./Picocrypt`) so you can read the crash message. If you still can't fix the crash, create an issue and ping me (@HACKERALERT). - type: markdown attributes: value: | @@ -40,7 +48,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 +56,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 +64,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 +72,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 +80,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 +92,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." diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml new file mode 100644 index 0000000..8b962ab --- /dev/null +++ b/.github/workflows/close-issues.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 6f6f5e6..e06e4dc 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ # Go workspace file go.work go.work.sum + +TODO diff --git a/Changelog.md b/Changelog.md index 8a81c2e..1adf30a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,13 @@ -# Future +# v1.48 (Released 04/18/2025) # v1.47 (Released 02/19/2025) diff --git a/README.md b/README.md index 1f06b82..b5491e9 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,6 @@ Picocrypt is a very small (hence Pico), 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 here. 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 here 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 here, 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: ``` diff --git a/VERSION b/VERSION index 99dd716..46284af 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.47 +1.48 \ No newline at end of file diff --git a/dist/flatpak/io.github.picocrypt.Picocrypt.metainfo.xml b/dist/flatpak/io.github.picocrypt.Picocrypt.metainfo.xml index b304d69..05ebab2 100644 --- a/dist/flatpak/io.github.picocrypt.Picocrypt.metainfo.xml +++ b/dist/flatpak/io.github.picocrypt.Picocrypt.metainfo.xml @@ -11,50 +11,35 @@ https://github.com/Picocrypt/Picocrypt https://github.com/Picocrypt/Picocrypt/issues - -

- 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. -

-

A list of features:

- +

Visit the project's GitHub repository to learn more.

- Utility Security - io.github.picocrypt.Picocrypt.desktop - https://github.com/Picocrypt/Picocrypt/raw/4d4bd47efe88ff25f372db81c4249920d399226b/images/screenshot.png + https://raw.githubusercontent.com/Picocrypt/Picocrypt/refs/heads/main/images/screenshot.png Main window - - https://github.com/Picocrypt/Picocrypt/blob/main/Changelog.md#v146-released-01292025 + + https://github.com/Picocrypt/Picocrypt/blob/main/Changelog.md#v148-released-04182025
    -
  • Added Picocrypt version to the window title.
  • -
  • Added ability to automatically unpack zip archives during decryption.
  • +
  • Allow pressing 'Enter' key to press Start/Process button
  • +
  • Update "Encrypt" button to "Zip and Encrypt" if multiple files
  • +
  • Give user estimated required free disk space in status label
  • +
  • Encrypt previously unencrypted temporary zip files
  • +
  • Add `.incomplete` to filenames while work is in progress
  • +
  • Use `encrypted-*.zip.pcv` output name instead of `Encrypted.zip.pcv`
  • +
  • Use 0700 permissions when auto unzipping and creating folders
  • +
  • Handle many more errors in the code where they were ignored previously
- - -
diff --git a/dist/windows/versioninfo.rc b/dist/windows/versioninfo.rc index 51e4afc..f2678e3 100644 --- a/dist/windows/versioninfo.rc +++ b/dist/windows/versioninfo.rc @@ -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" } diff --git a/src/Picocrypt.go b/src/Picocrypt.go index 10baa94..79e5049 100644 --- a/src/Picocrypt.go +++ b/src/Picocrypt.go @@ -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,7 @@ var startLabel = "Start" var mainStatus = "Ready" var mainStatusColor = WHITE var popupStatus string +var requiredFreeSpace int64 // Progress variables var progress float32 @@ -139,13 +141,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 +175,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 +425,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 +443,7 @@ func draw() { ), giu.Label(popupStatus), ).Build() - giu.OpenPopup(" ##" + strconv.Itoa(modalId)) + giu.OpenPopup("Progress:##" + strconv.Itoa(modalId)) giu.Update() } }), @@ -442,25 +586,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 err != nil || 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 +653,8 @@ func draw() { giu.Checkbox("Paranoid mode", ¶noid), 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 +669,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 +708,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 +758,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 @@ -640,6 +786,7 @@ func draw() { outputFile = file mainStatus = "Ready" mainStatusColor = WHITE + giu.Update() }).Build() giu.Tooltip("Save the output with a custom name and path").Build() }), @@ -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() { @@ -774,7 +855,7 @@ func onDrop(names []string) { duplicate = true } } - stat, _ := os.Stat(i) + stat, statErr := os.Stat(i) fin, err := os.Open(i) if err == nil { fin.Close() @@ -785,7 +866,7 @@ func onDrop(names []string) { giu.Update() return } - if !duplicate && !stat.IsDir() { + if !duplicate && statErr == nil && !stat.IsDir() { tmp = append(tmp, i) } } @@ -812,19 +893,26 @@ 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 + giu.Update() + 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 +950,7 @@ func onDrop(names []string) { totalFiles++ compressTotal += stat.Size() } + requiredFreeSpace = compressTotal } else { outputFile = names[0][:len(names[0])-4] } @@ -877,48 +966,84 @@ func onDrop(names []string) { if err != nil { resetUI() accessDenied("Read") + giu.Update() return } // Check if version can be read from header tmp := make([]byte, 15) - fin.Read(tmp) + if n, err := fin.Read(tmp); err != nil || n != 15 { + fin.Close() + mainStatus = "Failed to read 15 bytes from file" + mainStatusColor = RED + giu.Update() + return + } tmp, err = rsDecode(rs5, tmp) - if valid, _ := regexp.Match(`^v\d\.\d{2}`, tmp); !valid || err != nil { + if valid, _ := regexp.Match(`^v\d\.\d{2}`, tmp); err != nil || !valid { // Volume has plausible deniability deniability = true mainStatus = "Can't read header, assuming volume is deniable" fin.Close() + giu.Update() } else { // Read comments from file and check for corruption tmp = make([]byte, 15) - fin.Read(tmp) + if n, err := fin.Read(tmp); err != nil || n != 15 { + fin.Close() + mainStatus = "Failed to read 15 bytes from file" + mainStatusColor = RED + giu.Update() + return + } tmp, err = rsDecode(rs5, tmp) if err == nil { - commentsLength, _ := strconv.Atoi(string(tmp)) - tmp = make([]byte, commentsLength*3) - fin.Read(tmp) - comments = "" - for i := 0; i < commentsLength*3; i += 3 { - t, err := rsDecode(rs1, tmp[i:i+3]) - if err != nil { - comments = "Comments are corrupted" - break + commentsLength, err := strconv.Atoi(string(tmp)) + if err != nil { + comments = "Comment length is corrupted" + giu.Update() + } else { + tmp = make([]byte, commentsLength*3) + if n, err := fin.Read(tmp); err != nil || n != commentsLength*3 { + fin.Close() + mainStatus = "Failed to read comments from file" + mainStatusColor = RED + giu.Update() + return } - comments += string(t) + 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) + } + giu.Update() } } else { comments = "Comments are corrupted" + giu.Update() } // Read flags from file and check for corruption flags := make([]byte, 15) - fin.Read(flags) - fin.Close() + if n, err := fin.Read(flags); err != nil || n != 15 { + fin.Close() + mainStatus = "Failed to read 15 bytes from file" + mainStatusColor = RED + giu.Update() + return + } + if err := fin.Close(); err != nil { + panic(err) + } flags, err = rsDecode(rs5, flags) if err != nil { mainStatus = "The volume header is damaged" mainStatusColor = RED + giu.Update() return } @@ -932,6 +1057,7 @@ func onDrop(names []string) { if flags[2] == 1 { keyfileOrdered = true } + giu.Update() } } else { // One file was dropped for encryption mode = "encrypt" @@ -939,6 +1065,7 @@ func onDrop(names []string) { startLabel = "Encrypt" inputFile = names[0] outputFile = names[0] + ".pcv" + giu.Update() } // Add the file @@ -947,14 +1074,22 @@ func onDrop(names []string) { if !isSplit { compressTotal += stat.Size() } + giu.Update() } } 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 { - stat, _ := os.Stat(name) + stat, err := os.Stat(name) + if err != nil { + resetUI() + mainStatus = "Failed to stat dropped items" + mainStatusColor = RED + giu.Update() + return + } if stat.IsDir() { folders++ onlyFolders = append(onlyFolders, name) @@ -964,6 +1099,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,25 +1123,47 @@ 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" + giu.Update() } // Recursively add all files in 'onlyFolders' to 'allFiles' go func() { oldInputLabel := inputLabel for _, name := range onlyFolders { - filepath.Walk(name, func(path string, _ os.FileInfo, _ error) error { + if filepath.Walk(name, func(path string, _ os.FileInfo, err error) error { + if err != nil { + resetUI() + mainStatus = "Failed to walk through dropped items" + mainStatusColor = RED + giu.Update() + return err + } stat, err := os.Stat(path) + if err != nil { + resetUI() + mainStatus = "Failed to walk through dropped items" + mainStatusColor = RED + giu.Update() + return err + } // If 'path' is a valid file path, add to 'allFiles' - if err == nil && !stat.IsDir() { + if !stat.IsDir() { allFiles = append(allFiles, path) compressTotal += stat.Size() + requiredFreeSpace += stat.Size() inputLabel = fmt.Sprintf("Scanning files... (%s)", sizeify(compressTotal)) giu.Update() } return nil - }) + }) != nil { + resetUI() + mainStatus = "Failed to walk through dropped items" + mainStatusColor = RED + giu.Update() + return + } } inputLabel = fmt.Sprintf("%s (%s)", oldInputLabel, sizeify(compressTotal)) scanning = false @@ -1033,8 +1191,31 @@ 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() { // enclose to keep out of parent scope + key, nonce := make([]byte, 32), make([]byte, 12) + if n, err := rand.Read(key); err != nil || n != 32 { + panic(errors.New("fatal crypto/rand error")) + } + if n, err := rand.Read(nonce); err != nil || n != 12 { + panic(errors.New("fatal crypto/rand error")) + } + if bytes.Equal(key, make([]byte, 32)) || bytes.Equal(nonce, make([]byte, 12)) { + panic(errors.New("fatal crypto/rand error")) // this should never happen but be safe + } + var errW error + var errR error + tempZipCipherW, errW = chacha20.NewUnauthenticatedCipher(key, nonce) + tempZipCipherR, errR = chacha20.NewUnauthenticatedCipher(key, nonce) + if errW != nil || errR != nil { + panic(errors.New("fatal chacha20 init error")) + } + }() + // 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 +1231,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 +1239,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)) @@ -1067,9 +1253,24 @@ func work() { // Create file info header (size, last modified, etc.) stat, err := os.Stat(path) if err != nil { - continue // Skip temporary and inaccessible files + writer.Close() + file.Close() + os.Remove(inputFile) + resetUI() + mainStatus = "Failed to stat input files" + mainStatusColor = RED + return + } + header, err := zip.FileInfoHeader(stat) + if err != nil { + writer.Close() + file.Close() + os.Remove(inputFile) + resetUI() + mainStatus = "Failed to create zip.FileInfoHeader" + mainStatusColor = RED + return } - header, _ := zip.FileInfoHeader(stat) header.Name = strings.TrimPrefix(path, rootDir) header.Name = filepath.ToSlash(header.Name) header.Name = strings.TrimPrefix(header.Name, "/") @@ -1081,7 +1282,16 @@ func work() { } // Open the file for reading - entry, _ := writer.CreateHeader(header) + entry, err := writer.CreateHeader(header) + if err != nil { + writer.Close() + file.Close() + os.Remove(inputFile) + resetUI() + mainStatus = "Failed to writer.CreateHeader" + mainStatusColor = RED + return + } fin, err := os.Open(path) if err != nil { writer.Close() @@ -1112,8 +1322,12 @@ func work() { return } } - writer.Close() - file.Close() + if err := writer.Close(); err != nil { + panic(err) + } + if err := file.Close(); err != nil { + panic(err) + } } // Recombine a split file if necessary @@ -1149,7 +1363,7 @@ func work() { // Merge all chunks into one file startTime := time.Now() - for i := 0; i < totalFiles; i++ { + for i := range totalFiles { fin, err := os.Open(fmt.Sprintf("%s.%d", inputFile, i)) if err != nil { fout.Close() @@ -1173,10 +1387,11 @@ func work() { break } data = data[:read] - _, err = fout.Write(data) + var n int + n, err = fout.Write(data) done += read - if err != nil { + if err != nil || n != len(data) { insufficientSpace(fin, fout) os.Remove(outputFile + ".pcv") return @@ -1188,9 +1403,13 @@ func work() { popupStatus = fmt.Sprintf("Recombining at %.2f MiB/s (ETA: %s)", speed, eta) giu.Update() } - fin.Close() + if err := fin.Close(); err != nil { + panic(err) + } + } + if err := fout.Close(); err != nil { + panic(err) } - fout.Close() inputFileOld = inputFile inputFile = outputFile + ".pcv" } @@ -1204,26 +1423,44 @@ func work() { giu.Update() // Get size of volume for showing progress - stat, _ := os.Stat(inputFile) + stat, err := os.Stat(inputFile) + if err != nil { + // we already read from inputFile successfully in onDrop + // so it is very unlikely this err != nil, we can just panic + panic(err) + } total := stat.Size() // Rename input volume to free up the filename - fin, _ := os.Open(inputFile) + fin, err := os.Open(inputFile) + if err != nil { + panic(err) + } for strings.HasSuffix(inputFile, ".tmp") { inputFile = strings.TrimSuffix(inputFile, ".tmp") } inputFile += ".tmp" - fout, _ := os.Create(inputFile) + fout, err := os.Create(inputFile) + if err != nil { + panic(err) + } // Get the Argon2 salt and XChaCha20 nonce from input volume salt := make([]byte, 16) nonce := make([]byte, 24) - fin.Read(salt) - fin.Read(nonce) + if n, err := fin.Read(salt); err != nil || n != 16 { + panic(errors.New("failed to read 16 bytes from file")) + } + if n, err := fin.Read(nonce); err != nil || n != 24 { + panic(errors.New("failed to read 24 bytes from file")) + } // Generate key and XChaCha20 key := argon2.IDKey([]byte(password), salt, 4, 1<<20, 4, 32) - chacha, _ := chacha20.NewUnauthenticatedCipher(key, nonce) + chacha, err := chacha20.NewUnauthenticatedCipher(key, nonce) + if err != nil { + panic(err) + } // Decrypt the entire volume done, counter := 0, 0 @@ -1236,7 +1473,11 @@ func work() { src = src[:size] dst := make([]byte, len(src)) chacha.XORKeyStream(dst, src) - fout.Write(dst) + if n, err := fout.Write(dst); err != nil || n != len(dst) { + fout.Close() + os.Remove(fout.Name()) + panic(errors.New("failed to write dst")) + } // Update stats done += size @@ -1247,23 +1488,39 @@ func work() { // Change nonce after 60 GiB to prevent overflow if counter >= 60*GiB { tmp := sha3.New256() - tmp.Write(nonce) + if n, err := tmp.Write(nonce); err != nil || n != len(nonce) { + panic(errors.New("failed to write nonce to tmp during rekeying")) + } nonce = tmp.Sum(nil)[:24] - chacha, _ = chacha20.NewUnauthenticatedCipher(key, nonce) + chacha, err = chacha20.NewUnauthenticatedCipher(key, nonce) + if err != nil { + panic(err) + } counter = 0 } } - fin.Close() - fout.Close() + if err := fin.Close(); err != nil { + panic(err) + } + if err := fout.Close(); err != nil { + panic(err) + } // Check if the version can be read from the volume - fin, _ = os.Open(inputFile) + fin, err = os.Open(inputFile) + if err != nil { + panic(err) + } 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 { + if n, err := fin.Read(tmp); err != nil || n != 15 { + panic(errors.New("failed to read 15 bytes from file")) + } + if err := fin.Close(); err != nil { + panic(err) + } + tmp, err = rsDecode(rs5, tmp) + if valid, _ := regexp.Match(`^v1\.\d{2}`, tmp); err != nil || !valid { os.Remove(inputFile) inputFile = strings.TrimSuffix(inputFile, ".tmp") broken(nil, nil, "Password is incorrect or the file is not a volume", true) @@ -1280,7 +1537,12 @@ func work() { giu.Update() // Subtract the header size from the total size if decrypting - stat, _ := os.Stat(inputFile) + stat, err := os.Stat(inputFile) + if err != nil { + resetUI() + accessDenied("Read") + return + } total := stat.Size() if mode == "decrypt" { total -= 789 @@ -1318,7 +1580,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 { @@ -1385,6 +1647,18 @@ func work() { if _, err := rand.Read(nonce); err != nil { panic(err) } + if bytes.Equal(salt, make([]byte, 16)) { + panic(errors.New("fatal crypto/rand error")) + } + if bytes.Equal(hkdfSalt, make([]byte, 32)) { + panic(errors.New("fatal crypto/rand error")) + } + if bytes.Equal(serpentIV, make([]byte, 16)) { + panic(errors.New("fatal crypto/rand error")) + } + if bytes.Equal(nonce, make([]byte, 24)) { + panic(errors.New("fatal crypto/rand error")) + } // Encode values with Reed-Solomon and write to file _, errs[4] = fout.Write(rsEncode(rs16, salt)) @@ -1403,7 +1677,7 @@ func work() { if len(allFiles) > 1 || len(onlyFolders) > 0 || compress { os.Remove(inputFile) } - os.Remove(outputFile) + os.Remove(fout.Name()) return } } @@ -1507,6 +1781,9 @@ func work() { 32, ) } + if bytes.Equal(key, make([]byte, 32)) { + panic(errors.New("fatal crypto/argon2 error")) + } // If keyfiles are being used if len(keyfiles) > 0 || keyfile { @@ -1515,7 +1792,10 @@ func work() { var keyfileTotal int64 for _, path := range keyfiles { - stat, _ := os.Stat(path) + stat, err := os.Stat(path) + if err != nil { + panic(err) // we already checked os.Stat in onDrop + } keyfileTotal += stat.Size() } @@ -1525,7 +1805,10 @@ func work() { // For each keyfile... for _, path := range keyfiles { - fin, _ := os.Open(path) + fin, err := os.Open(path) + if err != nil { + panic(err) + } for { // Read in chunks of 1 MiB data := make([]byte, MiB) size, err := fin.Read(data) @@ -1533,27 +1816,36 @@ func work() { break } data = data[:size] - tmp.Write(data) // Hash the data + if _, err := tmp.Write(data); err != nil { // Hash the data + panic(err) + } // Update progress keyfileDone += size progress = float32(keyfileDone) / float32(keyfileTotal) giu.Update() } - fin.Close() + if err := fin.Close(); err != nil { + panic(err) + } } keyfileKey = tmp.Sum(nil) // Get the SHA3-256 // Store a hash of 'keyfileKey' for comparison tmp = sha3.New256() - tmp.Write(keyfileKey) + if _, err := tmp.Write(keyfileKey); err != nil { + panic(err) + } keyfileHash = tmp.Sum(nil) } else { // If order doesn't matter, hash individually and combine var keyfileDone int // For each keyfile... for _, path := range keyfiles { - fin, _ := os.Open(path) + fin, err := os.Open(path) + if err != nil { + panic(err) + } tmp := sha3.New256() for { // Read in chunks of 1 MiB data := make([]byte, MiB) @@ -1562,14 +1854,18 @@ func work() { break } data = data[:size] - tmp.Write(data) // Hash the data + if _, err := tmp.Write(data); err != nil { // Hash the data + panic(err) + } // Update progress keyfileDone += size progress = float32(keyfileDone) / float32(keyfileTotal) giu.Update() } - fin.Close() + if err := fin.Close(); err != nil { + panic(err) + } sum := tmp.Sum(nil) // Get the SHA3-256 @@ -1585,7 +1881,9 @@ func work() { // Store a hash of 'keyfileKey' for comparison tmp := sha3.New256() - tmp.Write(keyfileKey) + if _, err := tmp.Write(keyfileKey); err != nil { + panic(err) + } keyfileHash = tmp.Sum(nil) } } @@ -1595,7 +1893,9 @@ func work() { // Hash the encryption key for comparison when decrypting tmp := sha3.New512() - tmp.Write(key) + if _, err := tmp.Write(key); err != nil { + panic(err) + } keyHash = tmp.Sum(nil) // Validate the password and/or keyfiles @@ -1635,7 +1935,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 { @@ -1669,35 +1969,52 @@ func work() { } done, counter := 0, 0 - chacha, _ := chacha20.NewUnauthenticatedCipher(key, nonce) + chacha, err := chacha20.NewUnauthenticatedCipher(key, nonce) + if err != nil { + panic(err) + } // Use HKDF-SHA3 to generate a subkey for the MAC var mac hash.Hash subkey := make([]byte, 32) hkdf := hkdf.New(sha3.New256, key, hkdfSalt, nil) - hkdf.Read(subkey) + if n, err := hkdf.Read(subkey); err != nil || n != 32 { + panic(errors.New("fatal hkdf.Read error")) + } if paranoid { mac = hmac.New(sha3.New512, subkey) // HMAC-SHA3 } else { - mac, _ = blake2b.New512(subkey) // Keyed BLAKE2b + mac, err = blake2b.New512(subkey) // Keyed BLAKE2b + if err != nil { + panic(err) + } } // Generate another subkey for use as Serpent's key serpentKey := make([]byte, 32) - hkdf.Read(serpentKey) - s, _ := serpent.NewCipher(serpentKey) + if n, err := hkdf.Read(serpentKey); err != nil || n != 32 { + panic(errors.New("fatal hkdf.Read error")) + } + s, err := serpent.NewCipher(serpentKey) + if err != nil { + panic(err) + } serpent := cipher.NewCTR(s, serpentIV) // 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 +2025,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 } @@ -1723,7 +2046,9 @@ func work() { } chacha.XORKeyStream(dst, src) - mac.Write(dst) + if _, err := mac.Write(dst); err != nil { + panic(err) + } if reedsolo { copy(src, dst) @@ -1777,7 +2102,7 @@ func work() { } else { // Decode the full chunks chunks := len(dst)/136 - 1 - for i := 0; i < chunks; i++ { + for i := range chunks { tmp, err := rsDecode(rs128, dst[i*136:(i+1)*136]) if err != nil { if keep { @@ -1812,7 +2137,9 @@ func work() { dst = make([]byte, len(src)) } - mac.Write(src) + if _, err := mac.Write(src); err != nil { + panic(err) + } chacha.XORKeyStream(dst, src) if paranoid { @@ -1828,7 +2155,7 @@ func work() { if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress { os.Remove(inputFile) } - os.Remove(outputFile) + os.Remove(fout.Name()) return } @@ -1854,12 +2181,19 @@ func work() { if counter >= 60*GiB { // ChaCha20 nonce = make([]byte, 24) - hkdf.Read(nonce) - chacha, _ = chacha20.NewUnauthenticatedCipher(key, nonce) + if n, err := hkdf.Read(nonce); err != nil || n != 24 { + panic(errors.New("fatal hkdf.Read error")) + } + chacha, err = chacha20.NewUnauthenticatedCipher(key, nonce) + if err != nil { + panic(err) + } // Serpent serpentIV = make([]byte, 16) - hkdf.Read(serpentIV) + if n, err := hkdf.Read(serpentIV); err != nil || n != 16 { + panic(errors.New("fatal hkdf.Read error")) + } serpent = cipher.NewCTR(s, serpentIV) // Reset counter to 0 @@ -1876,10 +2210,18 @@ func work() { giu.Update() // Seek back to header and write important values - fout.Seek(int64(309+len(comments)*3), 0) - fout.Write(rsEncode(rs64, keyHash)) - fout.Write(rsEncode(rs32, keyfileHash)) - fout.Write(rsEncode(rs64, mac.Sum(nil))) + if _, err := fout.Seek(int64(309+len(comments)*3), 0); err != nil { + panic(err) + } + if _, err := fout.Write(rsEncode(rs64, keyHash)); err != nil { + panic(err) + } + if _, err := fout.Write(rsEncode(rs32, keyfileHash)); err != nil { + panic(err) + } + if _, err := fout.Write(rsEncode(rs64, mac.Sum(nil))); err != nil { + panic(err) + } } else { popupStatus = "Comparing values..." giu.Update() @@ -1904,8 +2246,16 @@ func work() { } } - fin.Close() - fout.Close() + if err := fin.Close(); err != nil { + panic(err) + } + if err := fout.Close(); err != nil { + panic(err) + } + + if err := os.Rename(outputFile+".incomplete", outputFile); err != nil { + panic(err) + } // Add plausible deniability if mode == "encrypt" && deniability { @@ -1914,29 +2264,51 @@ func work() { giu.Update() // Get size of volume for showing progress - stat, _ := os.Stat(fout.Name()) + stat, err := os.Stat(outputFile) + if err != nil { + panic(err) + } 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, err := os.Open(outputFile + ".tmp") + if err != nil { + panic(err) + } + fout, err := os.Create(outputFile + ".incomplete") + if err != nil { + panic(err) + } // Use a random Argon2 salt and XChaCha20 nonce salt := make([]byte, 16) nonce := make([]byte, 24) - if _, err := rand.Read(salt); err != nil { + if n, err := rand.Read(salt); err != nil || n != 16 { + panic(errors.New("fatal crypto/rand error")) + } + if n, err := rand.Read(nonce); err != nil || n != 24 { + panic(errors.New("fatal crypto/rand error")) + } + if bytes.Equal(salt, make([]byte, 16)) || bytes.Equal(nonce, make([]byte, 24)) { + panic(errors.New("fatal crypto/rand error")) + } + if _, err := fout.Write(salt); err != nil { panic(err) } - if _, err := rand.Read(nonce); err != nil { + if _, err := fout.Write(nonce); err != nil { panic(err) } - 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) + if bytes.Equal(key, make([]byte, 32)) { + panic(errors.New("fatal crypto/argon2 error")) + } + chacha, err := chacha20.NewUnauthenticatedCipher(key, nonce) + if err != nil { + panic(err) + } // Encrypt the entire volume done, counter := 0, 0 @@ -1949,7 +2321,9 @@ func work() { src = src[:size] dst := make([]byte, len(src)) chacha.XORKeyStream(dst, src) - fout.Write(dst) + if _, err := fout.Write(dst); err != nil { + panic(err) + } // Update stats done += size @@ -1960,16 +2334,30 @@ func work() { // Change nonce after 60 GiB to prevent overflow if counter >= 60*GiB { tmp := sha3.New256() - tmp.Write(nonce) + if _, err := tmp.Write(nonce); err != nil { + panic(err) + } nonce = tmp.Sum(nil)[:24] - chacha, _ = chacha20.NewUnauthenticatedCipher(key, nonce) + chacha, err = chacha20.NewUnauthenticatedCipher(key, nonce) + if err != nil { + panic(err) + } counter = 0 } } - fin.Close() - fout.Close() - os.Remove(fin.Name()) + if err := fin.Close(); err != nil { + panic(err) + } + if err := fout.Close(); err != nil { + panic(err) + } + if err := os.Remove(fin.Name()); err != nil { + panic(err) + } + if err := os.Rename(outputFile+".incomplete", outputFile); err != nil { + panic(err) + } canCancel = true giu.Update() } @@ -1977,11 +2365,17 @@ func work() { // Split the file into chunks if split { var splitted []string - stat, _ := os.Stat(outputFile) + stat, err := os.Stat(outputFile) + if err != nil { + panic(err) + } size := stat.Size() finishedFiles := 0 finishedBytes := 0 - chunkSize, _ := strconv.Atoi(splitSize) + chunkSize, err := strconv.Atoi(splitSize) + if err != nil { + panic(err) + } // Calculate chunk size if splitSelected == 0 { @@ -2002,19 +2396,27 @@ func work() { giu.Update() // Open the volume for reading - fin, _ := os.Open(outputFile) + fin, err := os.Open(outputFile) + if err != nil { + panic(err) + } // Delete existing chunks to prevent mixed chunks - names, _ := filepath.Glob(outputFile + ".*") + names, err := filepath.Glob(outputFile + ".*") + if err != nil { + panic(err) + } for _, i := range names { - os.Remove(i) + if err := os.Remove(i); err != nil { + panic(err) + } } // Start the splitting process startTime := time.Now() - for i := 0; i < chunks; i++ { + for i := range chunks { // 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 @@ -2066,7 +2468,9 @@ func work() { popupStatus = fmt.Sprintf("Splitting at %.2f MiB/s (ETA: %s)", speed, eta) giu.Update() } - fout.Close() + if err := fout.Close(); err != nil { + panic(err) + } // Update stats finishedFiles++ @@ -2078,8 +2482,21 @@ func work() { giu.Update() } - fin.Close() - os.Remove(outputFile) + if err := fin.Close(); err != nil { + panic(err) + } + if err := os.Remove(outputFile); err != nil { + panic(err) + } + names, err = filepath.Glob(outputFile + ".*.incomplete") + if err != nil { + panic(err) + } + for _, i := range names { + if err := os.Rename(i, strings.TrimSuffix(i, ".incomplete")); err != nil { + panic(err) + } + } } canCancel = false @@ -2089,7 +2506,9 @@ func work() { // Delete temporary files used during encryption and decryption if recombine || len(allFiles) > 1 || len(onlyFolders) > 0 || compress { - os.Remove(inputFile) + if err := os.Remove(inputFile); err != nil { + panic(err) + } if deniability { os.Remove(strings.TrimSuffix(inputFile, ".tmp")) } @@ -2108,21 +2527,31 @@ func work() { if err != nil { break } - os.Remove(fmt.Sprintf("%s.%d", inputFileOld, i)) + if err := os.Remove(fmt.Sprintf("%s.%d", inputFileOld, i)); err != nil { + panic(err) + } i++ } } else { - os.Remove(inputFile) + if err := os.Remove(inputFile); err != nil { + panic(err) + } if deniability { - os.Remove(strings.TrimSuffix(inputFile, ".tmp")) + if err := os.Remove(strings.TrimSuffix(inputFile, ".tmp")); err != nil { + panic(err) + } } } } else { for _, i := range onlyFiles { - os.Remove(i) + if err := os.Remove(i); err != nil { + panic(err) + } } for _, i := range onlyFolders { - os.RemoveAll(i) + if err := os.RemoveAll(i); err != nil { + panic(err) + } } } } @@ -2142,7 +2571,9 @@ func work() { return } - os.Remove(outputFile) + if err := os.Remove(outputFile); err != nil { + panic(err) + } } // All done, reset the UI @@ -2251,6 +2682,7 @@ func resetUI() { mainStatus = "Ready" mainStatusColor = WHITE popupStatus = "" + requiredFreeSpace = 0 progress = 0 progressInfo = "" @@ -2274,7 +2706,7 @@ func rsDecode(rs *infectious.FEC, data []byte) ([]byte, error) { } tmp := make([]infectious.Share, rs.Total()) - for i := 0; i < rs.Total(); i++ { + for i := range rs.Total() { tmp[i].Number = i tmp[i].Data = append(tmp[i].Data, data[i]) } @@ -2321,7 +2753,7 @@ func genPassword() string { chars += "-=_+!@#$^&()?<>" } tmp := make([]byte, passgenLength) - for i := 0; i < int(passgenLength); i++ { + for i := range int(passgenLength) { j, _ := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) tmp[i] = chars[j.Int64()] } @@ -2395,7 +2827,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 +2846,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 +2895,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) diff --git a/src/go.mod b/src/go.mod index 2d11f7b..e1efc59 100644 --- a/src/go.mod +++ b/src/go.mod @@ -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 diff --git a/src/go.sum b/src/go.sum index b6b9df4..7314822 100644 --- a/src/go.sum +++ b/src/go.sum @@ -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=