diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 633aa54..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,2 +0,0 @@ -.github/* @HACKERALERT -.github/workflows/* @HACKERALERT diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 3938344..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml deleted file mode 100644 index c2524a0..0000000 --- a/.github/workflows/release-build.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: release-build - -permissions: - contents: write - -on: - push: - tags: - - 'v[0-9].[0-9]+.[0-9]+' - - -jobs: - release-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '>=1.24' - check-latest: true - cache: false - - - name: Install packages - run: | - sudo apt update - sudo apt upgrade -y - sudo apt autoremove -y - sudo apt install -y gcc libgl1-mesa-dev libglu1-mesa xorg-dev libgtk-3-dev libxkbcommon-dev - - - name: Install dependencies - run: | - go mod download - go install -v github.com/fyne-io/fyne-cross@latest - - - name: Build Android - run: | - fyne-cross android -arch=arm64 - env: - CGO_ENABLED: 1 - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: PicoGo.apk - path: | - fyne-cross/dist/android-arm64/PicoGo.apk - if-no-files-found: error - compression-level: 9 - - - name: Generate checksums - run: | - echo "CHECKSUM_PICOGO_APK=$(sha256sum fyne-cross/dist-android-arm64/PicoGo.apk | cut -d ' ' -f1)" >> $GITHUB_ENV - - - name: Release - uses: softprops/action-gh-release@v2 - with: - files: | - fyne-cross/dist/android-arm64/PicoGo.apk - tag_name: ${{ env.VERSION }} - make_latest: true - append_body: true - body: | - **PicoGo:** - `sha256(PicoGo.apk): ${{ env.CHECKSUM_PICOGO_APK }}` \ No newline at end of file diff --git a/.github/workflows/test-build-android.yml b/.github/workflows/test-build-android.yml deleted file mode 100644 index 6f9613e..0000000 --- a/.github/workflows/test-build-android.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: test-build-android - -permissions: - contents: write - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test-build-android: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '>=1.24' - check-latest: true - cache: false - - - name: Install packages - run: | - sudo apt update - sudo apt upgrade -y - sudo apt autoremove -y - sudo apt install -y gcc libgl1-mesa-dev libglu1-mesa xorg-dev libgtk-3-dev libxkbcommon-dev - - - name: Install dependencies - run: | - go mod download - go install -v github.com/fyne-io/fyne-cross@latest - - - name: Build Android - run: | - fyne-cross android -arch=arm64 - env: - CGO_ENABLED: 1 - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: TEST-BUILD-DO-NOT-USE-THIS - path: | - fyne-cross/dist/android-arm64/* - if-no-files-found: error - compression-level: 9 diff --git a/.github/workflows/test-build-linux.yml b/.github/workflows/test-build-linux.yml deleted file mode 100644 index 87f5ba9..0000000 --- a/.github/workflows/test-build-linux.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: test-build-linux - -permissions: - contents: write - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test-build-linux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '>=1.24' - check-latest: true - cache: false - - - name: Install packages - run: | - sudo apt update - sudo apt upgrade -y - sudo apt autoremove -y - sudo apt install -y gcc libgl1-mesa-dev libglu1-mesa xorg-dev libgtk-3-dev libxkbcommon-dev - - - name: Install dependencies - run: | - go mod download - go install -v github.com/fyne-io/fyne-cross@latest - - - name: Build Linux - run: | - fyne-cross linux -arch=amd64 - env: - CGO_ENABLED: 1 - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: TEST-BUILD-DO-NOT-USE-THIS - path: | - fyne-cross/dist/linux-amd64/* - if-no-files-found: error - compression-level: 9 diff --git a/.github/workflows/test-build-windows.yml b/.github/workflows/test-build-windows.yml deleted file mode 100644 index 33b81b6..0000000 --- a/.github/workflows/test-build-windows.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: test-build-windows - -permissions: - contents: write - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test-build-windows: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '>=1.24' - check-latest: true - cache: false - - - name: Install packages - run: | - sudo apt update - sudo apt upgrade -y - sudo apt autoremove -y - sudo apt install -y gcc libgl1-mesa-dev libglu1-mesa xorg-dev libgtk-3-dev libxkbcommon-dev - - - name: Install dependencies - run: | - go mod download - go install -v github.com/fyne-io/fyne-cross@latest - - - name: Build Windows - run: | - fyne-cross windows -arch=amd64 - env: - CGO_ENABLED: 1 - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: TEST-BUILD-DO-NOT-USE-THIS - path: | - fyne-cross/dist/windows-amd64/* - if-no-files-found: error - compression-level: 9 diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml deleted file mode 100644 index 52cb72b..0000000 --- a/.github/workflows/test-coverage.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: test-coverage - -permissions: - contents: write - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test-coverage: - name: Go test coverage check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '>=1.24' - check-latest: true - cache: false - - - name: Install packages - run: | - sudo apt update - sudo apt upgrade -y - sudo apt autoremove -y - sudo apt install -y gcc libgl1-mesa-dev libglu1-mesa xorg-dev libgtk-3-dev libxkbcommon-dev - - - name: Install dependencies - run: | - go mod download - go install -v github.com/fyne-io/fyne-cross@latest - - - name: generate test coverage - run: go test ./... -coverprofile=./cover.out -covermode=atomic -coverpkg=./... -timeout 20m - - - name: check test coverage - uses: vladopajic/go-test-coverage@f080863892c102695c8066abc08aae12e3e94e1b # v2.13.1 - with: - config: ./.testcoverage.yml - git-token: ${{ github.ref_name == 'main' && secrets.GITHUB_TOKEN || '' }} - git-branch: badges diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b51c70b..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -cover.out diff --git a/.testcoverage.yml b/.testcoverage.yml deleted file mode 100644 index 7c0cc18..0000000 --- a/.testcoverage.yml +++ /dev/null @@ -1,8 +0,0 @@ -profile: cover.out -threshold: - # Minimum coverage percentage required for individual files. - file: 0 - # Minimum coverage percentage required for each package. - package: 0 - # Minimum overall project coverage percentage required. - total: 60 diff --git a/AndroidManifest.xml b/AndroidManifest.xml deleted file mode 100644 index f755b9b..0000000 --- a/AndroidManifest.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/FyneApp.toml b/FyneApp.toml deleted file mode 100644 index 1843f5b..0000000 --- a/FyneApp.toml +++ /dev/null @@ -1,8 +0,0 @@ -Website = "https://github.com/Picocrypt/PicoGo" - -[Details] -Icon = "icon.png" -Name = "PicoGo" -ID = "io.github.picocrypt.PicoGo" -Version = "0.1.0" -Build = 1 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.md b/README.md index c4568e5..0530f24 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -**Moved to https://github.com/njhuffman/picogo** +No longer supported. diff --git a/VERSION b/VERSION deleted file mode 100644 index 02bf650..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -v0.1.5 \ No newline at end of file diff --git a/cmd/picogo/main.go b/cmd/picogo/main.go deleted file mode 100644 index 130194a..0000000 --- a/cmd/picogo/main.go +++ /dev/null @@ -1,268 +0,0 @@ -package main - -import ( - "errors" - "flag" - "fmt" - "io" - "os" - "strings" - - "github.com/jschauma/getpass" - "github.com/picocrypt/picogo/internal/encryption" - "github.com/schollz/progressbar/v3" -) - -type args struct { - reedSolomon bool - paranoid bool - deniability bool - inFiles []string - keyfiles []string - keep bool - ordered bool - password string - comments string - overwrite bool - encryptOnly bool - decryptOnly bool -} - -var ( - reedSolomonFlag *bool - paranoidFlag *bool - deniabilityFlag *bool - keyfilesStrFlag *string - keepFlag *bool - orderedFlag *bool - passfromFlag *string - commentsFlag *string - overwriteFlag *bool - encryptOnlyFlag *bool - decryptOnlyFlag *bool -) - -func init() { - flag.Usage = func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] file1 [file2 ...]\n", os.Args[0]) - fmt.Fprintf(flag.CommandLine.Output(), "\nOptions:\n") - flag.PrintDefaults() - } - reedSolomonFlag = flag.Bool("rs", false, "(encryption) encode with Reed-Solomon bytes") - paranoidFlag = flag.Bool("paranoid", false, "(encryption) use paranoid mode") - deniabilityFlag = flag.Bool("deniability", false, "(encryption) use deniability mode") - keyfilesStrFlag = flag.String("keyfiles", "", "list of keyfiles to use. Separate list with commas (ex: keyfile1,keyfile2,keyfile3)") - keepFlag = flag.Bool("keep", false, "(decryption) keep output even if corrupted. If not set, the output file will be deleted.") - orderedFlag = flag.Bool("ordered", false, "(encryption) require keyfiles in given order") - passfromFlag = flag.String("passfrom", "tty", "password source. Options can be found in getpass documentation (github.com/jschauma/getpass)") - commentsFlag = flag.String("comments", "", "(encryption) comments to save with the file. THESE ARE NOT ENCRYPTED. Wrap in quotes.") - overwriteFlag = flag.Bool("overwrite", false, "overwrite existing files") - encryptOnlyFlag = flag.Bool("encrypt-only", false, "only handle files that require encryption (only processes non-.pcv files)") - decryptOnlyFlag = flag.Bool("decrypt-only", false, "only handle files that require decryption (only processes .pcv files)") - -} - -func parseArgs() (args, error) { - flag.Parse() - if flag.NArg() == 0 { - return args{}, errors.New("no file specified") - } - - password, err := getpass.Getpass(*passfromFlag) - if err != nil { - return args{}, fmt.Errorf("reading password from %s: %w", *passfromFlag, err) - } - keyfiles := []string{} - if len(*keyfilesStrFlag) > 0 { - keyfiles = strings.Split(*keyfilesStrFlag, ",") - } - - return args{ - reedSolomon: *reedSolomonFlag, - paranoid: *paranoidFlag, - deniability: *deniabilityFlag, - inFiles: flag.Args(), - keyfiles: keyfiles, - keep: *keepFlag, - ordered: *orderedFlag, - password: password, - comments: *commentsFlag, - overwrite: *overwriteFlag, - encryptOnly: *encryptOnlyFlag, - decryptOnly: *decryptOnlyFlag, - }, nil -} - -func openFiles(inFile string, keyfiles []string, outFile string, overwrite bool) (*os.File, []*os.File, *os.File, error) { - inReader, err := os.Open(inFile) - if err != nil { - return nil, nil, nil, fmt.Errorf("opening %s: %w", inFile, err) - } - keyfileReaders := []*os.File{} - for _, keyfile := range keyfiles { - reader, err := os.Open(keyfile) - if err != nil { - return nil, nil, nil, fmt.Errorf("opening %s: %w", keyfile, err) - } - keyfileReaders = append(keyfileReaders, reader) - } - _, err = os.Stat(outFile) - if err == nil && !overwrite { - return nil, nil, nil, fmt.Errorf("%s already exists", outFile) - } - outWriter, err := os.Create(outFile) - if err != nil { - return nil, nil, nil, fmt.Errorf("creating %s: %w", outFile, err) - } - return inReader, keyfileReaders, outWriter, nil -} - -func asReaders(files []*os.File) []io.Reader { - readers := make([]io.Reader, len(files)) - for i, file := range files { - readers[i] = file - } - return readers -} - -func encrypt( - inFile string, - keyfiles []string, - settings encryption.Settings, - password string, - outOf [2]int, - overwrite bool, -) error { - outFile := inFile + ".pcv" - inReader, keyfileReaders, outWriter, err := openFiles(inFile, keyfiles, outFile, overwrite) - if err != nil { - return fmt.Errorf("opening files: %w", err) - } - defer func() { - for _, reader := range append(keyfileReaders, inReader, outWriter) { - reader.Close() - } - }() - - tmp := make([]byte, encryption.HeaderSize(settings)) - _, err = outWriter.Write(tmp) - if err != nil { - return fmt.Errorf("writing blank header: %w", err) - } - - bar := progressbar.NewOptions( - -1, - progressbar.OptionShowBytes(true), - progressbar.OptionClearOnFinish(), - progressbar.OptionUseIECUnits(true), - progressbar.OptionSetDescription( - fmt.Sprintf("[%d/%d] Encrypting: %s", outOf[0]+1, outOf[1], inFile), - ), - ) - header, err := encryption.EncryptHeadless(inReader, password, asReaders(keyfileReaders), settings, io.MultiWriter(outWriter, bar)) - bar.Finish() // don't catch the error, fine to ignore - if err != nil { - return fmt.Errorf("encrypting %s: %w", inFile, err) - } - - _, err = outWriter.Seek(0, 0) - if err != nil { - return fmt.Errorf("seeking to start of %s: %w", outFile, err) - } - _, err = outWriter.Write(header) - if err != nil { - return fmt.Errorf("writing header to %s: %w", outFile, err) - } - fmt.Printf("[%d/%d] Encrypted %s to %s\n", outOf[0]+1, outOf[1], inFile, outFile) - return nil -} - -func decrypt( - inFile string, - keyfiles []string, - password string, - outOf [2]int, - keep bool, - overwrite bool, -) error { - outFile := strings.TrimSuffix(inFile, ".pcv") - inReader, keyfileReaders, outWriter, err := openFiles(inFile, keyfiles, outFile, overwrite) - if err != nil { - return fmt.Errorf("opening files: %w", err) - } - defer func() { - for _, reader := range append(keyfileReaders, inReader, outWriter) { - reader.Close() - } - }() - - bar := progressbar.NewOptions( - -1, - progressbar.OptionShowBytes(true), - progressbar.OptionClearOnFinish(), - progressbar.OptionUseIECUnits(true), - progressbar.OptionSetDescription( - fmt.Sprintf("[%d/%d] Decrypting: %s", outOf[0]+1, outOf[1], inFile), - ), - ) - damaged, err := encryption.Decrypt( - password, - asReaders(keyfileReaders), - inReader, - io.MultiWriter(outWriter, bar), - false, - ) - bar.Finish() // don't catch the error, fine to ignore - if err != nil { - if !keep { - removeErr := os.Remove(outFile) - if removeErr != nil { - fmt.Printf("error removing %s: %s\n", outFile, removeErr) - } - } - return err - } - if damaged { - fmt.Printf("Warning: %s is damaged but recovered with reed-solomon bytes. Consider re-encrypting the file.\n", inFile) - } - fmt.Printf("[%d/%d] Decrypted %s to %s\n", outOf[0]+1, outOf[1], inFile, outFile) - return nil -} - -func main() { - a, err := parseArgs() - if err != nil { - fmt.Printf("error reading args: %s\n", err) - os.Exit(1) - } - - for i, inFile := range a.inFiles { - if strings.HasSuffix(inFile, ".pcv") { - if a.encryptOnly { - fmt.Printf("[%d/%d] Skipping %s (encrypt-only is set)\n", i+1, len(a.inFiles), inFile) - continue - } - err := decrypt(inFile, a.keyfiles, a.password, [2]int{i, len(a.inFiles)}, a.keep, a.overwrite) - if err != nil { - fmt.Printf("error while decrypting %s: %s\n", inFile, err) - os.Exit(1) - } - } else { - if a.decryptOnly { - fmt.Printf("[%d/%d] Skipping %s (decrypt-only is set)\n", i+1, len(a.inFiles), inFile) - continue - } - settings := encryption.Settings{ - Comments: a.comments, - ReedSolomon: a.reedSolomon, - Paranoid: a.paranoid, - Deniability: a.deniability, - } - err := encrypt(inFile, a.keyfiles, settings, a.password, [2]int{i, len(a.inFiles)}, a.overwrite) - if err != nil { - fmt.Printf("error while encrypting %s: %s\n", inFile, err) - os.Exit(1) - } - } - } -} diff --git a/cmd/picogo/main_test.go b/cmd/picogo/main_test.go deleted file mode 100644 index 91b533f..0000000 --- a/cmd/picogo/main_test.go +++ /dev/null @@ -1,198 +0,0 @@ -package main - -import ( - "os" - "testing" - - "github.com/picocrypt/picogo/internal/encryption" -) - -func argsMatch(a, b args) bool { - if a.reedSolomon != b.reedSolomon { - return false - } - if a.paranoid != b.paranoid { - return false - } - if a.deniability != b.deniability { - return false - } - if len(a.keyfiles) != len(b.keyfiles) { - return false - } - for i := range a.keyfiles { - if a.keyfiles[i] != b.keyfiles[i] { - return false - } - } - if a.keep != b.keep { - return false - } - if a.ordered != b.ordered { - return false - } - if a.password != b.password { - return false - } - if a.comments != b.comments { - return false - } - if a.overwrite != b.overwrite { - return false - } - if a.encryptOnly != b.encryptOnly { - return false - } - if a.decryptOnly != b.decryptOnly { - return false - } - if len(a.inFiles) != len(b.inFiles) { - return false - } - for i := range a.inFiles { - if a.inFiles[i] != b.inFiles[i] { - return false - } - } - return true -} - -func TestParseArgs(t *testing.T) { - tests := []struct { - name string - args []string - want args - }{ - { - name: "basic case", - args: []string{"-passfrom", "pass:password", "file1.txt"}, - want: args{inFiles: []string{"file1.txt"}, password: "password"}, - }, - { - name: "all args", - args: []string{"-rs", "-paranoid", "-deniability", "-keyfiles", "keyfile1,keyfile2", "-keep", "-ordered", "-passfrom", "pass:password", "-comments", "test comments", "-overwrite", "-encrypt-only", "-decrypt-only", "file1.txt"}, - want: args{ - reedSolomon: true, - paranoid: true, - deniability: true, - keyfiles: []string{"keyfile1", "keyfile2"}, - keep: true, - ordered: true, - password: "password", - comments: "test comments", - overwrite: true, - encryptOnly: true, - decryptOnly: true, - inFiles: []string{"file1.txt"}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - os.Args = append([]string{"picogo"}, tt.args...) - got, err := parseArgs() - if err != nil { - t.Errorf("parseArgs() error = %v", err) - return - } - if !argsMatch(got, tt.want) { - t.Errorf("parseArgs() = %v, want %v", got, tt.want) - } - }) - } -} - -func fileExists(filename string) bool { - _, err := os.Stat(filename) - return !os.IsNotExist(err) -} - -func deleteFile(filename string) { - err := os.Remove(filename) - if err != nil { - panic(err) - } -} - -func TestDecrypt(t *testing.T) { - encryptedFilename := "../../internal/encryption/picocrypt_samples/v1.48/base1000.pcv" - decryptedFilename := "../../internal/encryption/picocrypt_samples/v1.48/base1000" - err := decrypt( - encryptedFilename, - []string{}, - "password", - [2]int{0, 2}, - false, - false, - ) - if err != nil { - t.Errorf("decrypt() error = %v", err) - } - if !fileExists(decryptedFilename) { - t.Errorf("decrypt() did not create file %s", decryptedFilename) - } - deleteFile(decryptedFilename) - - encryptedFilename = "../../internal/encryption/picocrypt_samples/v1.48/base1000_kf12.pcv" - decryptedFilename = "../../internal/encryption/picocrypt_samples/v1.48/base1000_kf12" - err = decrypt( - encryptedFilename, - []string{ - "../../internal/encryption/picocrypt_samples/keyfiles/keyfile1", - "../../internal/encryption/picocrypt_samples/keyfiles/keyfile2", - }, - "password", - [2]int{1, 2}, - false, - true, - ) - if err != nil { - t.Errorf("decrypt() error = %v", err) - } - if !fileExists(decryptedFilename) { - t.Errorf("decrypt() did not create file %s", decryptedFilename) - } - deleteFile(decryptedFilename) -} - -func TestEncrypt(t *testing.T) { - encryptedFilename := "../../internal/encryption/picocrypt_samples/basefiles/base1000.pcv" - decryptedFilename := "../../internal/encryption/picocrypt_samples/basefiles/base1000" - err := encrypt( - decryptedFilename, - []string{}, - encryption.Settings{}, - "password", - [2]int{0, 2}, - false, - ) - if err != nil { - t.Errorf("encrypt() error = %v", err) - } - if !fileExists(encryptedFilename) { - t.Errorf("encrypt() did not create file %s", encryptedFilename) - } - deleteFile(encryptedFilename) - - encryptedFilename = "../../internal/encryption/picocrypt_samples/basefiles/base1000.pcv" - decryptedFilename = "../../internal/encryption/picocrypt_samples/basefiles/base1000" - err = encrypt( - decryptedFilename, - []string{ - "../../internal/encryption/picocrypt_samples/keyfiles/keyfile1", - "../../internal/encryption/picocrypt_samples/keyfiles/keyfile2", - }, - encryption.Settings{}, - "password", - [2]int{1, 2}, - false, - ) - if err != nil { - t.Errorf("encrypt() error = %v", err) - } - if !fileExists(encryptedFilename) { - t.Errorf("encrypt() did not create file %s", encryptedFilename) - } - deleteFile(encryptedFilename) -} diff --git a/go.mod b/go.mod deleted file mode 100644 index cfc4380..0000000 --- a/go.mod +++ /dev/null @@ -1,52 +0,0 @@ -module github.com/picocrypt/picogo - -go 1.23.0 - -toolchain go1.24.1 - -require ( - fyne.io/fyne/v2 v2.6.1 - github.com/Picocrypt/infectious v0.0.0-20240830233326-3a050f65f9ec - github.com/Picocrypt/serpent v0.0.0-20240830233833-9ad6ab254fd7 - github.com/jschauma/getpass v0.2.3 - github.com/schollz/progressbar/v3 v3.18.0 - golang.org/x/crypto v0.40.0 -) - -require ( - fyne.io/systray v1.11.0 // indirect - github.com/BurntSushi/toml v1.4.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fredbi/uri v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/fyne-io/gl-js v0.1.0 // indirect - github.com/fyne-io/glfw-js v0.2.0 // indirect - github.com/fyne-io/image v0.1.1 // indirect - github.com/fyne-io/oksvg v0.1.0 // indirect - github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect - github.com/go-text/render v0.2.0 // indirect - github.com/go-text/typesetting v0.3.0 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/hack-pad/go-indexeddb v0.3.2 // indirect - github.com/hack-pad/safejs v0.1.0 // indirect - github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect - github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect - github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rymdport/portal v0.4.1 // indirect - github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect - github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect - github.com/stretchr/testify v1.10.0 // indirect - github.com/yuin/goldmark v1.7.8 // indirect - golang.org/x/image v0.24.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/term v0.33.0 // indirect - golang.org/x/text v0.27.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index a376746..0000000 --- a/go.sum +++ /dev/null @@ -1,100 +0,0 @@ -fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is= -fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU= -fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= -fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -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/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/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= -github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= -github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= -github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= -github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM= -github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= -github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM= -github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= -github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= -github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= -github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw= -github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= -github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= -github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= -github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= -github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= -github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= -github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= -github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= -github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= -github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= -github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= -github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= -github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= -github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc= -github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= -github.com/jschauma/getpass v0.2.3 h1:tJySlkGtHpT3JO1UBjxrLDSp9qvbb0gKRlcNslec10g= -github.com/jschauma/getpass v0.2.3/go.mod h1:mk40NJ7cAbcRkeDKDIajUsfQB/yweyiqjf29kyMnS18= -github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= -github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= -github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= -github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA= -github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= -github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA= -github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= -github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= -github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= -github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= -github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= -golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/icon.png b/icon.png deleted file mode 100644 index acfb25d..0000000 Binary files a/icon.png and /dev/null differ diff --git a/internal/encryption/ciphers.go b/internal/encryption/ciphers.go deleted file mode 100644 index 7af41bb..0000000 --- a/internal/encryption/ciphers.go +++ /dev/null @@ -1,226 +0,0 @@ -package encryption - -import ( - "crypto/cipher" - "fmt" - "io" - - "github.com/Picocrypt/serpent" - "golang.org/x/crypto/chacha20" - "golang.org/x/crypto/sha3" -) - -const resetNonceAt = int64(60 * (1 << 30)) - -type nonceManager interface { - nonce(i int) ([24]byte, error) -} - -type ivManager interface { - iv(i int) ([16]byte, error) -} - -type nonceIvManager struct { - chachaNonces [][24]byte - serpentIVs [][16]byte - seeds *seeds - hkdf io.Reader -} - -func (nm *nonceIvManager) extendTo(i int) error { - if len(nm.chachaNonces) == 0 { - nm.chachaNonces = [][24]byte{nm.seeds.Nonce} - nm.serpentIVs = [][16]byte{nm.seeds.SerpentIV} - } - for i >= len(nm.chachaNonces) { - chachaNonce := [24]byte{} - serpentIV := [16]byte{} - _, err := io.ReadFull(nm.hkdf, chachaNonce[:]) - if err != nil { - return err - } - _, err = io.ReadFull(nm.hkdf, serpentIV[:]) - if err != nil { - return err - } - nm.chachaNonces = append(nm.chachaNonces, chachaNonce) - nm.serpentIVs = append(nm.serpentIVs, serpentIV) - } - return nil -} - -func (nm *nonceIvManager) nonce(i int) ([24]byte, error) { - err := nm.extendTo(i) - if err != nil { - return [24]byte{}, err - } - return nm.chachaNonces[i], nil -} - -func (nm *nonceIvManager) iv(i int) ([16]byte, error) { - err := nm.extendTo(i) - if err != nil { - return [16]byte{}, err - } - return nm.serpentIVs[i], nil -} - -func newNonceManager(hkdf io.Reader, seeds *seeds) *nonceIvManager { - nm := &nonceIvManager{ - seeds: seeds, - hkdf: hkdf, - } - return nm -} - -type denyNonceManager struct { - nonces [][24]byte - header *header -} - -func (dnm *denyNonceManager) extendTo(i int) error { - if len(dnm.nonces) == 0 { - dnm.nonces = append(dnm.nonces, dnm.header.seeds.DenyNonce) - } - for i >= len(dnm.nonces) { - previous := dnm.nonces[len(dnm.nonces)-1] - tmp := sha3.New256() - _, err := tmp.Write(previous[:]) - if err != nil { - return err - } - nonce := [24]byte{} - copy(nonce[:], tmp.Sum(nil)) - dnm.nonces = append(dnm.nonces, nonce) - } - return nil -} - -func (dnm *denyNonceManager) nonce(i int) ([24]byte, error) { - err := dnm.extendTo(i) - if err != nil { - return [24]byte{}, err - } - return dnm.nonces[i], nil -} - -type serpentCipher struct { - serpentBlock cipher.Block - cipher cipher.Stream - ivManager ivManager - header *header -} - -func (sc *serpentCipher) reset(i int) error { - serpentIV, err := sc.ivManager.iv(i) - if err != nil { - return err - } - sc.cipher = cipher.NewCTR(sc.serpentBlock, serpentIV[:]) - return nil -} - -func (sc *serpentCipher) xor(p []byte) { - sc.cipher.XORKeyStream(p, p) -} - -type chachaCipher struct { - cipher *chacha20.Cipher - nonceManager nonceManager - key []byte -} - -func (cc *chachaCipher) reset(i int) error { - nonce, err := cc.nonceManager.nonce(i) - if err != nil { - return err - } - cc.cipher, err = chacha20.NewUnauthenticatedCipher(cc.key[:], nonce[:]) - return err -} - -func (cc *chachaCipher) xor(p []byte) { - cc.cipher.XORKeyStream(p, p) -} - -type xorCipher interface { - xor(p []byte) - reset(i int) error -} - -type rotatingCipher struct { - xorCipher - writtenCounter int64 - resetCounter int - initialised bool -} - -func (rc *rotatingCipher) stream(p []byte) ([]byte, error) { - if !rc.initialised { - err := rc.xorCipher.reset(0) - if err != nil { - return nil, err - } - rc.initialised = true - } - i := int64(0) - for i < int64(len(p)) { - j := int64(len(p)) - i - if j > (resetNonceAt - rc.writtenCounter) { - j = resetNonceAt - rc.writtenCounter - } - rc.xor(p[i : i+j]) - rc.writtenCounter += j - if rc.writtenCounter == resetNonceAt { - rc.writtenCounter = 0 - rc.resetCounter++ - err := rc.reset(rc.resetCounter) - if err != nil { - return nil, err - } - } - i += j - } - return p, nil -} - -func (rc *rotatingCipher) flush() ([]byte, error) { - return nil, nil -} - -func newDeniabilityStream(password string, header *header) streamerFlusher { - nonceManager := denyNonceManager{header: header} - denyKey := generateDenyKey(password, header.seeds.DenySalt) - return &rotatingCipher{ - xorCipher: &chachaCipher{ - nonceManager: &nonceManager, - key: denyKey[:], - }, - } -} - -func newEncryptionStreams(keys keys, header *header) ([]streamerFlusher, error) { - nonceIvManager := newNonceManager(keys.hkdf, &(header.seeds)) - chachaStream := &rotatingCipher{ - xorCipher: &chachaCipher{ - nonceManager: nonceIvManager, - key: keys.key[:], - }, - } - if !header.settings.Paranoid { - return []streamerFlusher{chachaStream}, nil - } - sb, err := serpent.NewCipher(keys.serpentKey[:]) - if err != nil { - return nil, fmt.Errorf("creating serpent cipher: %w", err) - } - serpentStream := &rotatingCipher{ - xorCipher: &serpentCipher{ - serpentBlock: sb, - ivManager: nonceIvManager, - header: header, - cipher: nil, // will be set during streaming - }, - } - return []streamerFlusher{chachaStream, serpentStream}, nil -} diff --git a/internal/encryption/consistency_test.go b/internal/encryption/consistency_test.go deleted file mode 100644 index d1200b8..0000000 --- a/internal/encryption/consistency_test.go +++ /dev/null @@ -1,172 +0,0 @@ -package encryption - -import ( - "bytes" - "crypto/rand" - "io" - "log" - mrand "math/rand/v2" - "testing" - - "golang.org/x/crypto/sha3" -) - -func shaArgonKey(password string, salt [16]byte, iterations uint32, parallelism uint8) [32]byte { - // faster stand-in for testing - data := append(append(append([]byte(password), salt[:]...), byte(iterations)), byte(parallelism)) - hasher := sha3.New256() - _, err := hasher.Write(data) - if err != nil { - panic(err) - } - key := [32]byte{} - copy(key[:], hasher.Sum(nil)) - return key -} - -func allSettings() []Settings { - settings := []Settings{} - for _, comments := range []string{"", "test"} { - for _, reedSolomon := range []bool{true, false} { - for _, paranoid := range []bool{true, false} { - for _, orderedKf := range []bool{true, false} { - for _, deniability := range []bool{true, false} { - s := Settings{comments, reedSolomon, paranoid, orderedKf, deniability} - settings = append(settings, s) - } - } - } - } - } - return settings -} - -func getKeyfiles(settings Settings, numKeyfiles int, t *testing.T) ([]io.Reader, []io.Reader) { - kf1, kf2 := []io.Reader{}, []io.Reader{} - for i := 0; i < numKeyfiles; i++ { - key1 := make([]byte, 100) - _, err := rand.Read(key1) - if err != nil { - t.Fatal(err) - } - key2 := make([]byte, len(key1)) - copy(key2, key1) - kf1 = append(kf1, bytes.NewBuffer(key1)) - kf2 = append(kf2, bytes.NewBuffer(key2)) - } - if !settings.OrderedKf { - for i := len(kf2) - 1; i > 0; i-- { - j := mrand.IntN(i + 1) - kf2[i], kf2[int(j)] = kf2[int(j)], kf2[i] - } - } - return kf1, kf2 -} - -func randomPassword() string { - password := "" - n := mrand.IntN(100) + 1 - for i := 0; i < n; i++ { - char := mrand.IntN(128) - password += string(byte(char)) - } - return password -} - -func randomData(size int) ([]byte, error) { - data := make([]byte, size) - _, err := rand.Read(data) - if err != nil { - return nil, err - } - return data, nil -} - -func buff(data []byte) *bytes.Buffer { - d := make([]byte, len(data)) - copy(d, data) - return bytes.NewBuffer(d) -} - -func testConsistency(settings Settings, size int, numKeyfiles int, t *testing.T) { - original, err := randomData(size) - if err != nil { - t.Fatal("opening file:", err) - } - - password := randomPassword() - kf1, kf2 := getKeyfiles(settings, numKeyfiles, t) - - headless := bytes.NewBuffer([]byte{}) - header, err := EncryptHeadless(buff(original), password, kf1, settings, headless) - if err != nil { - t.Fatal(err) - } - headlessLen := headless.Len() - headed := bytes.NewBuffer([]byte{}) - err = PrependHeader(headless, headed, header) - if err != nil { - t.Fatal(err) - } - encryptedLen := headed.Len() - encrypted, err := io.ReadAll(headed) - if err != nil { - t.Fatal(err) - } - decrypted := bytes.NewBuffer([]byte{}) - Decrypt(password, kf2, buff(encrypted), decrypted, false) - - result, err := io.ReadAll(decrypted) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(result, original) { - log.Println("original length:", len(original)) - log.Println("headless length:", headlessLen) - log.Println("headed length:", encryptedLen) - log.Println("decrypted length:", len(result)) - log.Println("missing bytes:", len(original)-len(result)) - t.Fatal("decryption does not match") - } - -} - -func TestConsistencyEmptyFile(t *testing.T) { - // test for an empty file - argonKey = shaArgonKey - for _, settings := range allSettings() { - for numKeyfiles := range 3 { - testConsistency(settings, 0, numKeyfiles, t) - } - } -} - -func TestConsistencySmallFile(t *testing.T) { - // test for a file size less than readSize - argonKey = shaArgonKey - for _, settings := range allSettings() { - for numKeyfiles := range 3 { - testConsistency(settings, readSize/2, numKeyfiles, t) - } - } -} - -func TestConsistencyLargeFile(t *testing.T) { - // test for a file size greater than readSize - argonKey = shaArgonKey - for _, settings := range allSettings() { - for numKeyfiles := range 3 { - testConsistency(settings, readSize*2+500, numKeyfiles, t) - } - } -} - -func TestConsistencyReadSize(t *testing.T) { - // test for a file exactly at readSize - argonKey = shaArgonKey - for _, settings := range allSettings() { - for numKeyfiles := range 3 { - testConsistency(settings, readSize, numKeyfiles, t) - } - } -} diff --git a/internal/encryption/decrypt.go b/internal/encryption/decrypt.go deleted file mode 100644 index 77ff2a9..0000000 --- a/internal/encryption/decrypt.go +++ /dev/null @@ -1,448 +0,0 @@ -package encryption - -import ( - "bytes" - "crypto/hmac" - "errors" - "fmt" - "hash" - "io" - "regexp" - "strconv" - "strings" - - "golang.org/x/crypto/blake2b" - "golang.org/x/crypto/sha3" -) - -type damageTracker struct { - damage bool -} - -type buffer struct { - size int - data []byte -} - -func (b *buffer) isFull() bool { - return len(b.data) == b.size -} - -func (b *buffer) add(p []byte) []byte { - idx := len(p) - if len(p) > b.size-len(b.data) { - idx = b.size - len(b.data) - } - if idx > 0 { - b.data = append(b.data, p[:idx]...) - return p[idx:] - } - return p -} - -func parseVersion(versionBytes []byte) (bool, string, error) { - if len(versionBytes) != (versionSize * 3) { - return false, "", fmt.Errorf("invalid version length: %d", len(versionBytes)) - } - v := make([]byte, versionSize) - damaged, _, err := rsDecode(v, versionBytes[:], false) - if err != nil { - return damaged, "", fmt.Errorf("decoding version: %w", err) - } - valid, err := regexp.Match(`^v1\.\d{2}`, v) - if err != nil { - return damaged, "", fmt.Errorf("parsing version format: %w", err) - } - if !valid { - return damaged, "", nil - } - return damaged, string(v), nil -} - -type versionStream struct { - buff buffer - damageTracker *damageTracker -} - -func (v *versionStream) stream(p []byte) ([]byte, error) { - if !v.buff.isFull() { - p = v.buff.add(p) - if v.buff.isFull() { - // check that the version is actually good - damaged, version, err := parseVersion(v.buff.data) - v.damageTracker.damage = v.damageTracker.damage || damaged - if err != nil { - return nil, fmt.Errorf("parsing version: %w", err) - } - if version == "" { - return nil, ErrHeaderCorrupted - } - } - } - return p, nil -} - -func makeVersionStream(damageTracker *damageTracker) *versionStream { - return &versionStream{ - buff: buffer{size: versionSize * 3}, - damageTracker: damageTracker, - } -} - -type deniabilityStream struct { - password string - buff buffer - deny streamer - header *header -} - -func (d *deniabilityStream) stream(p []byte) ([]byte, error) { - if !d.buff.isFull() { - p = d.buff.add(p) - if d.buff.isFull() { - // Don't catch damaged flag, it is handled by the version stream - _, version, err := parseVersion(d.buff.data[:versionSize*3]) - if err != nil { - return nil, fmt.Errorf("parsing version: %w", err) - } - if version != "" { - d.header.settings.Deniability = false - p = append(d.buff.data, p...) - } else { - d.header.settings.Deniability = true - salt := [16]byte{} - nonce := [24]byte{} - copy(salt[:], d.buff.data[:len(salt)]) - copy(nonce[:], d.buff.data[len(salt):]) - d.header.seeds.DenyNonce = nonce - d.header.seeds.DenySalt = salt - d.deny = newDeniabilityStream(d.password, d.header) - } - } - } - if d.deny != nil { - var err error - p, err = d.deny.stream(p) - if err != nil { - return nil, fmt.Errorf("denying data: %w", err) - } - } - return p, nil -} - -func makeHeaderDeniabilityStream(password string, header *header) *deniabilityStream { - return &deniabilityStream{ - password: password, - buff: buffer{size: 16 + 24}, // 16 bytes for salt, 24 bytes for nonce - header: header, - deny: nil, // will be set during streaming - } -} - -type flagStream struct { - buff buffer - header *header - damageTracker *damageTracker -} - -func (f *flagStream) stream(p []byte) ([]byte, error) { - if !f.buff.isFull() { - p = f.buff.add(p) - if f.buff.isFull() { - data := make([]byte, flagsSize) - damaged, corrupted, err := rsDecode(data, f.buff.data, false) - f.damageTracker.damage = f.damageTracker.damage || damaged - if corrupted { - return nil, ErrHeaderCorrupted - } - if err != nil { - return nil, fmt.Errorf("decoding flags: %w", err) - } - f.header.settings.Paranoid = data[0] == 1 - f.header.usesKf = data[1] == 1 - f.header.settings.OrderedKf = data[2] == 1 - f.header.settings.ReedSolomon = data[3] == 1 - f.header.nearMiBFlag = data[4] == 1 - } - } - return p, nil -} - -func makeFlagStream(header *header, damageTracker *damageTracker) *flagStream { - return &flagStream{buffer{size: flagsSize * 3}, header, damageTracker} -} - -type commentStream struct { - lenBuff buffer - commentBuff buffer - header *header - damageTracker *damageTracker -} - -func (c *commentStream) stream(p []byte) ([]byte, error) { - if !c.lenBuff.isFull() { - p = c.lenBuff.add(p) - if c.lenBuff.isFull() { - cLenRune := make([]byte, commentSize) - damaged, corrupted, err := rsDecode(cLenRune, c.lenBuff.data, false) - c.damageTracker.damage = c.damageTracker.damage || damaged - if corrupted { - return nil, ErrHeaderCorrupted - } - if err != nil { - return nil, fmt.Errorf("decoding comment length: %w", err) - } - cLen, err := strconv.Atoi(string(cLenRune)) - if err != nil { - return nil, fmt.Errorf("parsing comment length: %w", ErrHeaderCorrupted) - } - if (cLen < 0) || (cLen > maxCommentsLength) { - return nil, ErrHeaderCorrupted - } - c.commentBuff = buffer{size: cLen * 3} - } - } - if c.lenBuff.isFull() && !c.commentBuff.isFull() { - p = c.commentBuff.add(p) - if c.commentBuff.isFull() { - var builder strings.Builder - for i := 0; i < len(c.commentBuff.data); i += 3 { - value := [1]byte{} - // Ignore corruption on comments, they are not critical - damaged, _, err := rsDecode(value[:], c.commentBuff.data[i:i+3], false) - c.damageTracker.damage = c.damageTracker.damage || damaged - if err != nil { - return nil, fmt.Errorf("decoding comment length: %w", err) - } - builder.WriteByte(value[0]) - } - c.header.settings.Comments = builder.String() - } - } - return p, nil -} - -func makeCommentStream(header *header, damageTracker *damageTracker) *commentStream { - return &commentStream{ - lenBuff: buffer{size: commentSize * 3}, - header: header, - damageTracker: damageTracker, - } -} - -type sliceStream struct { - buff buffer - slice []byte - damageTracker *damageTracker -} - -func (s *sliceStream) stream(p []byte) ([]byte, error) { - if !s.buff.isFull() { - p = s.buff.add(p) - if s.buff.isFull() { - data := make([]byte, len(s.slice)) - damaged, corrupted, err := rsDecode(data, s.buff.data, false) - s.damageTracker.damage = s.damageTracker.damage || damaged - if corrupted { - return nil, ErrHeaderCorrupted - } - if err != nil { - return nil, fmt.Errorf("decoding slice: %w", err) - } - copy(s.slice, data) - } - } - return p, nil -} - -func makeSliceStream(slice []byte, damagedTracker *damageTracker) *sliceStream { - return &sliceStream{buffer{size: len(slice) * 3}, slice, damagedTracker} -} - -type headerStream struct { - header *header - streams []streamer - isDone func() bool -} - -func (h *headerStream) stream(p []byte) ([]byte, error) { - return streamStack(h.streams, p) -} - -func makeHeaderStream(password string, header *header, damageTracker *damageTracker) *headerStream { - macTagStream := makeSliceStream(header.refs.macTag[:], damageTracker) - isDone := func() bool { - return macTagStream.buff.isFull() - } - streams := []streamer{ - makeHeaderDeniabilityStream(password, header), - makeVersionStream(damageTracker), - makeCommentStream(header, damageTracker), - makeFlagStream(header, damageTracker), - makeSliceStream(header.seeds.Salt[:], damageTracker), - makeSliceStream(header.seeds.HkdfSalt[:], damageTracker), - makeSliceStream(header.seeds.SerpentIV[:], damageTracker), - makeSliceStream(header.seeds.Nonce[:], damageTracker), - makeSliceStream(header.refs.keyRef[:], damageTracker), - makeSliceStream(header.refs.keyfileRef[:], damageTracker), - macTagStream, - } - return &headerStream{header, streams, isDone} -} - -func getHeader(r io.Reader, password string) (header, error) { - h := header{} - stream := makeHeaderStream(password, &h, &damageTracker{}) - for { - p := make([]byte, 1000) // big enough to get most headers in one read - n, err := r.Read(p) - eof := errors.Is(err, io.EOF) - if err != nil && !eof { - return header{}, fmt.Errorf("reading file: %w", err) - } - _, err = stream.stream(p[:n]) - if err != nil { - return header{}, fmt.Errorf("reading header: %w", err) - } - if stream.isDone() { - return h, nil - } - if eof { - return header{}, ErrFileTooShort - } - } -} - -type macStream struct { - mac hash.Hash - encrypting bool - header *header -} - -func (ms *macStream) stream(p []byte) ([]byte, error) { - _, err := ms.mac.Write(p) - if err != nil { - return nil, err - } - return p, nil -} - -func (ms *macStream) flush() ([]byte, error) { - m := ms.mac.Sum(nil) - if ms.encrypting { - copy(ms.header.refs.macTag[:], m) - return nil, nil - } - if !hmac.Equal(m, ms.header.refs.macTag[:]) { - return nil, ErrBodyCorrupted - } - return nil, nil -} - -func newMacStream(keys keys, header *header, encrypting bool) (*macStream, error) { - var mac hash.Hash - if header.settings.Paranoid { - mac = hmac.New(sha3.New512, keys.macKey[:]) - } else { - var err error - mac, err = blake2b.New512(keys.macKey[:]) - if err != nil { - return nil, err - } - } - return &macStream{mac: mac, header: header, encrypting: encrypting}, nil -} - -type decryptStream struct { - password string - keyfiles []io.Reader - headerStream *headerStream - bodyStreams []streamerFlusher - damageTracker *damageTracker -} - -func (ds *decryptStream) stream(p []byte) ([]byte, error) { - p, err := ds.headerStream.stream(p) - if err != nil { - return nil, err - } - if ds.headerStream.isDone() { - if ds.bodyStreams == nil { - ds.bodyStreams, err = ds.makeBodyStreams() - if err != nil { - return nil, err - } - } - return streamStack(ds.bodyStreams, p) - } - return p, nil -} - -func (ds *decryptStream) flush() ([]byte, error) { - if !ds.headerStream.isDone() { - return nil, ErrFileTooShort - } - if ds.bodyStreams == nil { - return nil, nil - } - return flushStack(ds.bodyStreams) -} - -func (ds *decryptStream) makeBodyStreams() ([]streamerFlusher, error) { - // TODO implement keyfiles - keys, err := validateKeys(ds.headerStream.header, ds.password, ds.keyfiles) - if err != nil { - // TODO should I include duplicate keyfiles error here? - return nil, err - } - // TODO verify that the keyRef matches the header - streams := []streamerFlusher{} - // TODO: add reed solomon if configured - if ds.headerStream.header.settings.ReedSolomon { - streams = append(streams, makeRSDecodeStream(false, ds.headerStream.header, ds.damageTracker)) - } - macStream, err := newMacStream(keys, ds.headerStream.header, false) - if err != nil { - return nil, err - } - streams = append(streams, macStream) - encryptionStreams, err := newEncryptionStreams(keys, ds.headerStream.header) - if err != nil { - return nil, err - } - streams = append(streams, encryptionStreams...) - return streams, nil -} - -func makeDecryptStream(password string, keyfiles []io.Reader, damageTracker *damageTracker) *decryptStream { - header := header{} - return &decryptStream{ - password: password, - keyfiles: keyfiles, - headerStream: makeHeaderStream(password, &header, damageTracker), - damageTracker: damageTracker, - } -} - -func validateKeys(header *header, password string, keyfiles []io.Reader) (keys, error) { - if header.usesKf && len(keyfiles) == 0 { - return keys{}, ErrKeyfilesRequired - } - if !header.usesKf && len(keyfiles) > 0 { - return keys{}, ErrKeyfilesNotRequired - } - keys, err := newKeys(header.settings, header.seeds, password, keyfiles) - if err != nil && !errors.Is(err, ErrDuplicateKeyfiles) { - return keys, err - } - if !bytes.Equal(keys.keyRef[:], header.refs.keyRef[:]) { - return keys, ErrIncorrectPassword - } - if header.usesKf && !bytes.Equal(keys.keyfileRef[:], header.refs.keyfileRef[:]) { - if header.settings.OrderedKf { - return keys, ErrIncorrectOrMisorderedKeyfiles - } - return keys, ErrIncorrectKeyfiles - } - return keys, nil -} diff --git a/internal/encryption/docs/code_architecture.md b/internal/encryption/docs/code_architecture.md deleted file mode 100644 index d231b5b..0000000 --- a/internal/encryption/docs/code_architecture.md +++ /dev/null @@ -1,274 +0,0 @@ -# Code Architecture - -Here are some block diagrams of how the code is structured. Not all details are shown, but these diagrams give a reasonably cohesive idea of how data is passed through and modified. - -# Decryption -## Low Detail View - -Data is first passed through `Header Decryption` which is responsible for initializing the shared `Header` state with correct values, consuming all header-related bytes, and undoing any encryption from deniability mode. By the time data is passed out of `Header Decryption`, the `Header` object is fully initialized and ready to be used. Any damaged bytes are reported to the shared `DamageTracker`. - -Data then passes through `Body Decryption` which is responsible for decoding the file data and checking it for correctness. The data output by `Body Decryption` is fully decoded. Any damaged bytes are reported to the shared `DamageTracker`. - -```mermaid -flowchart TB - - subgraph SH[Header Decryption] - direction LR - - SH_S1[[Deniability Wrapper]] - SH_S2[[Header Fields]] - - SH_A[/Incoming bytes/] --> SH_S1 --> SH_S2 --> SH_B[\Outgoing bytes\] - - end - - subgraph SB[Body Decryption] - direction LR - - SB_A[/Incoming bytes/] - SB_S1[[ReedSolomon]] - SB_S2[[MAC]] - SB_S3[[Encryption]] - SB_SB[\Outgoing bytes\] - - SB_A --> SB_S1 - SB_S1 --> SB_S2 - SB_S2 --> SB_S3 - SB_S3 --> SB_SB - end - - A[/Encrypted data/] --> SH --> SB --> B[/Decrypted data/] - - HEADER[(Header)] - DMG[(DamageTracker)] -``` - -## High Detail View - -```mermaid -flowchart TB - - subgraph SH[Header Decryption] - direction TB - - subgraph SH_S1[Deniability Wrapper] - direction LR - SH_S1_HEADER[(Header)] - SH_S1_A[/Incoming bytes/] --> SH_S1_B{Are the first bytes a valid version?} - SH_S1_B --> |Yes| SH_S1_C[\Outgoing bytes\] - SH_S1_B --> |No| SH_S1_D[Consume the first 40 bytes] - SH_S1_D -.-> |denyMode, denyNonce, denySalt| SH_S1_HEADER - SH_S1_D --> SH_S1_E[ChaCha20 Cipher] - SH_S1_HEADER -.-> |denyNonce, denySalt| SH_S1_E - SH_S1_E --> SH_S1_C - end - - subgraph SH_S2[Header Fields] - direction LR - SH_S2_HEADER[(Header)] - SH_S2_DMG[(DamageTracker)] - SH_S2_A[/Incoming bytes/] --> SH_S2_B{{"Loop over fields in [version, comments, flags, salt, hkdfSalt, serpentIV, nonce, keyRef, keyfileRef, macTag]"}} - SH_S2_B --> |field| SH_S2_C{"Are bytes for field decodable?"} - SH_S2_C --> |No| SH_S2_D(Consume all bytes) - SH_S2_D -.-> |damaged| SH_S2_DMG - SH_S2_C --> |Yes| SH_S2_E{Was error correction required?} - SH_S2_E --> |Yes| SH_S2_F[Record damage] - SH_S2_F -.-> |damaged| SH_S2_DMG - SH_S2_F --> SH_S2_G[Consume field bytes] - SH_S2_E --> |No| SH_S2_G - SH_S2_G --> SH_S2_B - SH_S2_G -.-> |field| SH_S2_HEADER - SH_S2_B --> |done| SH_S2_H[\Outgoing bytes\] - - SH_S5_I>"Note: actual implementation unrolls the loop"] - end - - SH_S1 --> SH_S2 - - end - - subgraph SB[Body Decryption] - direction TB - - subgraph SB_S1[ReedSolomon] - SB_S1_HEADER[(Header)] - SB_S1_DMG[(DamageTracker)] - - SB_S1_A[/Incoming bytes/] --> SB_S1_B{Has Reed Solomon encoding?} - SB_S1_B --> |Yes| SB_S1_C[Break into 136 byte chunks] - SB_S1_C --> SB_S1_D[Consume Reed Solomon bytes] - SB_S1_D --> SB_S1_E{Is chunk decodable?} - SB_S1_E --> |Yes| SB_S1_F{Was error correction required?} - SB_S1_F --> |Yes| SB_S1_G[Record damage] - SB_S1_G -.-> |damaged| SB_S1_DMG - SB_S1_I -.-> |damaged| SB_S1_DMG - SB_S1_G --> SB_S1_H[\Outgoing bytes\] - SB_S1_E --> |No| SB_S1_I[Use first bytes as best guess] - SB_S1_I --> SB_S1_H - SB_S1_F --> |No| SB_S1_H - SB_S1_HEADER -.-> |ReedSolomon| SB_S1_B - SB_S1_B --> |No| SB_S1_H - end - - subgraph SB_S2[MAC] - direction LR - SB_S2_HEADER[(Header)] - SB_S2_DMG[(DamageTracker)] - SB_S2_A[/Incoming bytes/] --> SB_S2_B{Encrypted with Paranoid mode?} - SB_S2_B --> |Yes| SB_S2_C["Record to SHA3.512 (does not modify bytes)"] - SB_S2_B --> |No| SB_S2_D["Record to Blake2 (does not modify bytes)"] - SB_S2_E[\Outgoing bytes\] - SB_S2_C --> SB_S2_E - SB_S2_D --> SB_S2_E - - SB_S2_F{Does final sum match macTag?} - SB_S2_G(Do nothing) - SB_S2_H(Report error) - SB_S2_C -.-> |On flush| SB_S2_F - SB_S2_D -.-> |On flush| SB_S2_F - SB_S2_F -.-> |Yes| SB_S2_G - SB_S2_F -.-> |No| SB_S2_H - SB_S2_H -.-> |damaged| SB_S2_DMG - - SB_S2_HEADER -.-> |paranoid| SB_S2_B - SB_S2_HEADER -.-> |macTag| SB_S2_F - end - - subgraph SB_S3[Encryption] - direction LR - SB_S3_HEADER[(Header)] - SB_S3_A[/Incoming bytes/] - SB_S3_B[ChaCha20 cipher] - SB_S3_C{Is paranoid?} - SB_S3_D[Serpent] - SB_S3_E[\Outgoing bytes\] - SB_S3_A --> SB_S3_B --> SB_S3_C - SB_S3_C --> |Yes| SB_S3_D --> SB_S3_E - SB_S3_C --> |No| SB_S3_E - SB_S3_HEADER -.-> |nonce, salt| SB_S3_B - SB_S3_HEADER -.-> |paranoid| SB_S3_C - SB_S3_HEADER -.-> |iv| SB_S3_D - end - - SB_S1 --> SB_S2 --> SB_S3 - - end - - A[/Data to decrypt/] --> SH --> SB --> B[/Decrypted data/] -``` - -# Encryption - -Encryption is broken into 2 steps: encode the file itself through `Encryption Stream`, then combine the `Header` bytes with the encoded body data. These steps are separated because some header data such as the `macTag` cannot be known until the file data has been fully encrypted. The header fields required for `Encryption Stream` are initialized from the passed `Settings` and randomly generated seeds. - - -```mermaid -flowchart LR - - A[Settings] - B[Random Seeds] - C[Header] - D[[Encryption Stream]] - E[/Input File/] - F[header + body] - H[Encrypted body data] - I[\Output File\] - J[Apply deniability if enabled] - - A -.-> C - B -.-> C - C -.-> D - E --> D - D --> H - H --> |body bytes| F - C --> |After body data is encrypted| J --> |header bytes| F - F --> I -``` - -## Low Detail View - -Low detail view of the `Encryption Stream`. It is very similar to running the `Decryption Stream` in reverse. Data is first passed through `Primary Encryption` (ChaCha20, plus Serpent if paranoid). Then a mac is computed and saved to the header to check against when decrypting. Then Reed-Solomon bytes are added if requested. Then everything is passed through `Deniability Wrapper` (another ChaCha20) if requested. The output bytes are the full body, ready to be appended to the header bytes, which can now be computed. - -```mermaid -flowchart TB - - subgraph SB[Body Encryption] - direction LR - SB_S1[[Encryption]] - SB_S2[[MAC]] - SB_S3[[ReedSolomon]] - SB_S4[[Deniability Wrapper]] - SB_S1 --> SB_S2 --> SB_S3 --> SB_S4 - end - - A[/Data to encrypt/] --> SB --> B[/Encrypted data/] -``` - -## High Detail View - -```mermaid -flowchart TB - - subgraph SB[Body Encryption] - direction TB - - subgraph SB_S1[Encryption] - direction LR - SB_S1_HEADER[(Header)] - SB_S1_A[/Incoming bytes/] - SB_S1_B[ChaCha20 cipher] - SB_S1_C{Is paranoid?} - SB_S1_D[Serpent] - SB_S1_E[\Outgoing bytes\] - SB_S1_A --> SB_S1_B --> SB_S1_C - SB_S1_C --> |No| SB_S1_E - SB_S1_C --> |Yes| SB_S1_D --> SB_S1_E - SB_S1_HEADER -.-> |nonce, salt| SB_S1_B - SB_S1_HEADER -.-> |paranoid| SB_S1_C - SB_S1_HEADER -.-> |iv| SB_S1_D - end - - subgraph SB_S2[MAC] - direction LR - SB_S2_HEADER[(Header)] - SB_S2_A[/Incoming bytes/] --> SB_S2_B{Paranoid Mode?} - SB_S2_B --> |Yes| SB_S2_C["Record to SHA3.512 (does not modify bytes)"] - SB_S2_B --> |No| SB_S2_D["Record to Blake2 (does not modify bytes)"] - SB_S2_E[\Outgoing bytes\] - SB_S2_C --> SB_S2_E - SB_S2_D --> SB_S2_E - - SB_S2_F(Save sum to header) - SB_S2_C -.-> |On flush| SB_S2_F - SB_S2_D -.-> |On flush| SB_S2_F - SB_S2_F -.-> |macTag| SB_S2_HEADER - - SB_S2_HEADER -.-> |paranoid| SB_S2_B - end - - subgraph SB_S3[ReedSolomon] - SB_S3_HEADER[(Header)] - SB_S3_A[/Incoming bytes/] --> SB_S3_B{Reed Solomon encoding?} - SB_S3_B --> |Yes| SB_S3_C[Break into 128 byte chunks] - SB_S3_C --> SB_S3_D[Add 8 Reed Solomon bytes per chunk] - SB_S3_D --> SB_S3_E[\Outgoing bytes\] - SB_S3_HEADER -.-> |ReedSolomon| SB_S3_B - SB_S3_B --> |No| SB_S3_E - end - - subgraph SB_S4[Deniability Wrapper] - direction LR - SH_S4_HEADER[(Header)] - SH_S4_A[/Incoming bytes/] --> SH_S4_B{Deniability mode?} - SH_S4_B --> |No| SH_S4_C[\Outgoing bytes\] - SH_S4_B --> |Yes| SH_S4_D["ChaCha20 Cipher (seeded with mock header bytes)"] - SH_S4_HEADER -.-> |denyNonce, denySalt| SH_S4_D - SH_S4_D --> SH_S4_C - end - - SB_S1 --> SB_S2 --> SB_S3 --> SB_S4 - - end - - A[/Data to encrypt/] --> SB --> B[/Encrypted data/] -``` \ No newline at end of file diff --git a/internal/encryption/encrypt.go b/internal/encryption/encrypt.go deleted file mode 100644 index 37bac7b..0000000 --- a/internal/encryption/encrypt.go +++ /dev/null @@ -1,90 +0,0 @@ -package encryption - -import ( - "fmt" - "io" -) - -var picocryptVersion = "v1.48" - -type sizeStream struct { - header *header - counter int64 -} - -func (s *sizeStream) stream(p []byte) ([]byte, error) { - s.counter += int64(len(p)) - return p, nil -} - -func (s *sizeStream) flush() ([]byte, error) { - s.header.nearMiBFlag = (s.counter % (1 << 20)) > ((1 << 20) - chunkSize) - return nil, nil -} - -type encryptStream struct { - header *header - streams []streamerFlusher -} - -func (es *encryptStream) stream(p []byte) ([]byte, error) { - return streamStack(es.streams, p) -} - -func (es *encryptStream) flush() ([]byte, error) { - return flushStack(es.streams) -} - -func makeEncryptStream(settings Settings, seeds seeds, password string, keyfiles []io.Reader) (*encryptStream, error) { - keys, err := newKeys(settings, seeds, password, keyfiles) - if err != nil { - return nil, fmt.Errorf("generating keys: %w", err) - } - header := header{ - settings: settings, - seeds: seeds, - refs: refs{ - keyRef: keys.keyRef, - keyfileRef: keys.keyfileRef, - macTag: [64]byte{}, // will be filled by mac stream - }, - usesKf: len(keyfiles) > 0, - nearMiBFlag: false, - } - - streams := []streamerFlusher{} - - encryptionStreams, err := newEncryptionStreams(keys, &header) - if err != nil { - return nil, fmt.Errorf("creating encryption stream: %w", err) - } - streams = append(streams, encryptionStreams...) - - macStream, err := newMacStream(keys, &header, true) - if err != nil { - return nil, fmt.Errorf("creating mac stream: %w", err) - } - streams = append(streams, macStream) - - sizeStream := sizeStream{header: &header} - streams = append(streams, &sizeStream) - - if settings.ReedSolomon { - streams = append(streams, makeRSEncodeStream(&header)) - } - - if settings.Deniability { - deniabilityStream := newDeniabilityStream(password, &header) - mockHeaderData := make([]byte, baseHeaderSize+3*len(settings.Comments)) - _, err := deniabilityStream.stream(mockHeaderData) - if err != nil { - return nil, fmt.Errorf("seeding deniability stream: %w", err) - } - streams = append(streams, deniabilityStream) - } - - return &encryptStream{ - header: &header, - streams: streams, - }, nil -} diff --git a/internal/encryption/encryption.go b/internal/encryption/encryption.go deleted file mode 100644 index 7dbb7e5..0000000 --- a/internal/encryption/encryption.go +++ /dev/null @@ -1,190 +0,0 @@ -package encryption - -import ( - "errors" - "fmt" - "io" -) - -const readSize = 1 << 20 -const maxCommentsLength = 99999 - -var ErrFileTooShort = errors.New("file too short") -var ErrIncorrectPassword = errors.New("incorrect password") -var ErrIncorrectKeyfiles = errors.New("incorrect keyfiles") -var ErrIncorrectOrMisorderedKeyfiles = errors.New("incorrect or misordered keyfiles") -var ErrKeyfilesRequired = errors.New("missing required keyfiles") -var ErrDuplicateKeyfiles = errors.New("duplicate keyfiles") -var ErrKeyfilesNotRequired = errors.New("keyfiles not required") -var ErrHeaderCorrupted = errors.New("header corrupted") -var ErrBodyCorrupted = errors.New("body corrupted") -var ErrCommentsTooLong = errors.New("comments exceed maximum length") - -type Settings struct { - Comments string - ReedSolomon bool - Paranoid bool - OrderedKf bool - Deniability bool -} - -func Decrypt( - pw string, - kf []io.Reader, - r io.Reader, - w io.Writer, - skipReedSolomon bool, -) (bool, error) { - - damageTracker := damageTracker{} - decryptStream := makeDecryptStream(pw, kf, &damageTracker) - - for { - p := make([]byte, readSize) - n, err := r.Read(p) - eof := false - if err != nil { - if errors.Is(err, io.EOF) { - eof = true - } else { - return false, fmt.Errorf("reading input: %w", err) - } - } - p, err = decryptStream.stream(p[:n]) - if err != nil { - return damageTracker.damage, err - } - _, err = w.Write(p) - if err != nil { - return damageTracker.damage, err - } - if eof { - p, err := decryptStream.flush() - if (err == nil) || errors.Is(err, ErrBodyCorrupted) { - _, e := w.Write(p) - return damageTracker.damage, e - } - return damageTracker.damage, err - } - } -} - -func GetEncryptionSettings(r io.Reader) (Settings, error) { - header, err := getHeader(r, "") - if errors.Is(err, ErrFileTooShort) { - return Settings{Deniability: true}, nil - } - if err != nil { - return Settings{}, fmt.Errorf("reading header: %w", err) - } - return header.settings, nil -} - -func EncryptHeadless( - in io.Reader, - password string, - keyfiles []io.Reader, - settings Settings, - out io.Writer, -) ([]byte, error) { - if len(settings.Comments) > maxCommentsLength { - return nil, ErrCommentsTooLong - } - seeds, err := randomSeeds() - if err != nil { - return nil, fmt.Errorf("generating seeds: %w", err) - } - return encryptWithSeeds(in, password, keyfiles, settings, out, seeds) -} - -func encryptWithSeeds( - in io.Reader, - password string, - keyfiles []io.Reader, - settings Settings, - out io.Writer, - seeds seeds, -) ([]byte, error) { - encryptionStream, err := makeEncryptStream(settings, seeds, password, keyfiles) - if err != nil { - return nil, fmt.Errorf("making encryption stream: %w", err) - } - - buf := make([]byte, readSize) - for { - eof := false - n, err := in.Read(buf) - if err != nil { - if errors.Is(err, io.EOF) { - eof = true - } else { - return nil, fmt.Errorf("reading input: %w", err) - } - } - p, err := encryptionStream.stream(buf[:n]) - if err != nil { - return nil, fmt.Errorf("encrypting input: %w", err) - } - _, err = out.Write(p) - if err != nil { - return nil, fmt.Errorf("writing output: %w", err) - } - if eof { - break - } - } - - p, err := encryptionStream.flush() - if err != nil { - return nil, fmt.Errorf("closing encryptor: %w", err) - } - _, err = out.Write(p) - if err != nil { - return nil, fmt.Errorf("writing output: %w", err) - } - - headerBytes, err := encryptionStream.header.bytes(password) - if err != nil { - return nil, fmt.Errorf("making header: %w", err) - } - return headerBytes, nil -} - -func PrependHeader( - headless io.Reader, - headed io.Writer, - header []byte, -) error { - _, err := headed.Write(header) - if err != nil { - return fmt.Errorf("writing header: %w", err) - } - - for { - data := make([]byte, readSize) - n, err := headless.Read(data) - eof := err == io.EOF - if (err != nil) && (err != io.EOF) { - return fmt.Errorf("reading body data: %w", err) - } - data = data[:n] - - _, err = headed.Write(data) - if err != nil { - return fmt.Errorf("writing body data: %w", err) - } - - if eof { - break - } - } - return nil -} - -func HeaderSize(settings Settings) int { - size := baseHeaderSize + 3*len(settings.Comments) - if settings.Deniability { - size += len(seeds{}.DenyNonce) + len(seeds{}.DenySalt) - } - return size -} diff --git a/internal/encryption/expected_errors_test.go b/internal/encryption/expected_errors_test.go deleted file mode 100644 index 5c20475..0000000 --- a/internal/encryption/expected_errors_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package encryption - -import ( - "bytes" - "crypto/rand" - "errors" - "io" - "os" - "testing" -) - -func TestFileTooShort(t *testing.T) { - argonKey = argon2IDKey - for size := range []int{0, 10} { - invalidData := make([]byte, size) - _, err := rand.Read(invalidData) - if err != nil { - t.Fatal("creating random data:", err) - } - _, err = Decrypt("password", []io.Reader{}, bytes.NewBuffer(invalidData), bytes.NewBuffer([]byte{}), false) - if !errors.Is(err, ErrFileTooShort) { - t.Fatal("expected ErrFileTooShort, got", err) - } - } -} - -func TestHeaderCorrupted(t *testing.T) { - argonKey = argon2IDKey - invalidData := make([]byte, 1000) - _, err := rand.Read(invalidData) - if err != nil { - t.Fatal("creating random data:", err) - } - _, err = Decrypt("password", []io.Reader{}, bytes.NewBuffer(invalidData), bytes.NewBuffer([]byte{}), false) - if !errors.Is(err, ErrHeaderCorrupted) { - t.Fatal("expected ErrHeaderCorrupted, got", err) - } -} - -func TestIncorrectPassword(t *testing.T) { - argonKey = argon2IDKey - reader, err := os.Open("picocrypt_samples/v1.48/base1000.pcv") - if err != nil { - t.Fatal("opening file:", err) - } - defer reader.Close() - _, err = Decrypt("wrong-password", []io.Reader{}, reader, bytes.NewBuffer([]byte{}), false) - if !errors.Is(err, ErrIncorrectPassword) { - t.Fatal("expected wrong password, got", err) - } -} - -func TestIncorrectKeyfiles(t *testing.T) { - argonKey = argon2IDKey - reader, err := os.Open("picocrypt_samples/v1.48/base0_kf12o.pcv") - if err != nil { - t.Fatal("opening file:", err) - } - wrongKeyfileData := make([]byte, 100) - rand.Read(wrongKeyfileData) - defer reader.Close() - _, err = Decrypt("password", []io.Reader{bytes.NewBuffer(wrongKeyfileData)}, reader, bytes.NewBuffer([]byte{}), false) - if !errors.Is(err, ErrIncorrectOrMisorderedKeyfiles) { - t.Fatal("expected wrong keyfieles, got", err) - } -} - -func TestKeyfilesRequired(t *testing.T) { - argonKey = argon2IDKey - reader, err := os.Open("picocrypt_samples/v1.48/base0_kf12o.pcv") - if err != nil { - t.Fatal("opening file:", err) - } - defer reader.Close() - _, err = Decrypt("password", []io.Reader{}, reader, bytes.NewBuffer([]byte{}), false) - if !errors.Is(err, ErrKeyfilesRequired) { - t.Fatal("expected required keyfiles, got", err) - } -} - -func TestDuplicateKeyfiles(t *testing.T) { - argonKey = argon2IDKey - keyfileData := make([]byte, 100) - rand.Read(keyfileData) - keyfiles := []io.Reader{} - for range 2 { - buff := make([]byte, len(keyfileData)) - copy(buff, keyfileData) - keyfiles = append(keyfiles, bytes.NewBuffer(buff)) - } - _, err := EncryptHeadless( - bytes.NewBuffer([]byte{}), - "test-password", - keyfiles, - Settings{}, - bytes.NewBuffer([]byte{}), - ) - if !errors.Is(err, ErrDuplicateKeyfiles) { - t.Fatal("expected ErrDuplicateKeyfiles, got", err) - } -} - -func TestKeyfilesNotRequired(t *testing.T) { - argonKey = argon2IDKey - reader, err := os.Open("picocrypt_samples/v1.48/base1000.pcv") - if err != nil { - t.Fatal("opening file:", err) - } - defer reader.Close() - _, err = Decrypt("password", []io.Reader{bytes.NewBuffer([]byte{})}, reader, bytes.NewBuffer([]byte{}), false) - if !errors.Is(err, ErrKeyfilesNotRequired) { - t.Fatal("expected ErrKeyfilesNotRequired, got", err) - } -} - -func TestCorrupted(t *testing.T) { - argonKey = argon2IDKey - reader, err := os.Open("picocrypt_samples/v1.48/base1000.pcv") - if err != nil { - t.Fatal("opening file:", err) - } - defer reader.Close() - r := bytes.NewBuffer([]byte{}) - _, err = io.Copy(r, reader) - if err != nil { - t.Fatal("reading file:", err) - } - rawBytes := r.Bytes() - copy(rawBytes[:], []byte("corrupted")) - corruptedReader := bytes.NewBuffer(rawBytes) - _, err = Decrypt("qwerty", []io.Reader{}, corruptedReader, bytes.NewBuffer([]byte{}), false) - if !errors.Is(err, ErrHeaderCorrupted) { - t.Fatal("expected ErrHeaderCorrupted, got", err) - } -} - -func TestDamagedButRecoverable(t *testing.T) { - argonKey = argon2IDKey - reader, err := os.Open("picocrypt_samples/v1.48/base1000_r.pcv") - if err != nil { - t.Fatal("opening file:", err) - } - defer reader.Close() - - encryptedData, err := io.ReadAll(reader) - if err != nil { - t.Fatal("reading file:", err) - } - - encryptedData[1000] = byte(int(encryptedData[1000]) + 1) - - damaged, err := Decrypt("password", []io.Reader{}, bytes.NewBuffer(encryptedData), bytes.NewBuffer([]byte{}), false) - if err != nil { - t.Fatal("expected no error, got", err) - } - if !damaged { - t.Fatal("expected damage") - } -} - -func TestCommentsTooLong(t *testing.T) { - argonKey = argon2IDKey - comments := make([]byte, maxCommentsLength+1) - _, err := EncryptHeadless( - bytes.NewBuffer([]byte{}), - "test-password", - []io.Reader{}, - Settings{Comments: string(comments)}, - bytes.NewBuffer([]byte{}), - ) - if !errors.Is(err, ErrCommentsTooLong) { - t.Fatal("expected ErrCommentsTooLong, got", err) - } -} diff --git a/internal/encryption/header.go b/internal/encryption/header.go deleted file mode 100644 index 2c836fb..0000000 --- a/internal/encryption/header.go +++ /dev/null @@ -1,102 +0,0 @@ -package encryption - -import ( - "bytes" - "crypto/rand" - "encoding/binary" - "fmt" - "io" -) - -const ( - baseHeaderSize = 789 - versionSize = 5 - commentSize = 5 - flagsSize = 5 -) - -type seeds struct { - // export to allow binary package to fill - Salt [16]byte - Nonce [24]byte - SerpentIV [16]byte - HkdfSalt [32]byte - DenySalt [16]byte - DenyNonce [24]byte -} - -type refs struct { - keyRef [64]byte - keyfileRef [32]byte - macTag [64]byte -} - -type header struct { - settings Settings - seeds seeds - refs refs - usesKf bool - nearMiBFlag bool // set when the original data is within 1 chunk of a MiB -} - -func (header *header) bytes(password string) ([]byte, error) { - data := [][]byte{[]byte(picocryptVersion)} - data = append(data, []byte(fmt.Sprintf("%05d", len(header.settings.Comments)))) - for _, c := range []byte(header.settings.Comments) { - data = append(data, []byte{c}) - } - flags := []bool{ - header.settings.Paranoid, - header.usesKf, - header.settings.OrderedKf, - header.settings.ReedSolomon, - header.nearMiBFlag, - } - flagBytes := make([]byte, len(flags)) - for i, f := range flags { - if f { - flagBytes[i] = 1 - } - } - data = append(data, flagBytes) - data = append(data, header.seeds.Salt[:]) - data = append(data, header.seeds.HkdfSalt[:]) - data = append(data, header.seeds.SerpentIV[:]) - data = append(data, header.seeds.Nonce[:]) - data = append(data, header.refs.keyRef[:]) - data = append(data, header.refs.keyfileRef[:]) - data = append(data, header.refs.macTag[:]) - - headerBytes := make([]byte, baseHeaderSize+3*len(header.settings.Comments)) - written := 0 - for _, d := range data { - err := rsEncode(headerBytes[written:written+len(d)*3], d) - if err != nil { - return nil, err - } - written += len(d) * 3 - } - - if header.settings.Deniability { - denyStream := newDeniabilityStream(password, header) - var err error - headerBytes, err = denyStream.stream(headerBytes) - if err != nil { - return nil, fmt.Errorf("denying header data: %w", err) - } - headerBytes = append(append(header.seeds.DenySalt[:], header.seeds.DenyNonce[:]...), headerBytes...) - } - - return headerBytes, nil -} - -func randomSeeds() (seeds, error) { - raw := make([]byte, binary.Size(seeds{})) - _, err := io.ReadFull(rand.Reader, raw) - if err != nil { - return seeds{}, err - } - decoded := seeds{} - err = binary.Read(bytes.NewBuffer(raw), binary.BigEndian, &decoded) - return decoded, err -} diff --git a/internal/encryption/keys.go b/internal/encryption/keys.go deleted file mode 100644 index ec1e590..0000000 --- a/internal/encryption/keys.go +++ /dev/null @@ -1,162 +0,0 @@ -package encryption - -import ( - "bytes" - "errors" - "fmt" - "hash" - "io" - - "golang.org/x/crypto/argon2" - "golang.org/x/crypto/hkdf" - "golang.org/x/crypto/sha3" -) - -type keys struct { - key [32]byte - macKey [32]byte - serpentKey [32]byte - denyKey [32]byte - hkdf io.Reader - keyRef [64]byte - keyfileRef [32]byte -} - -func xor(a, b [32]byte) [32]byte { - var result [32]byte - for i := range a { - result[i] = a[i] ^ b[i] - } - return result -} - -func generateKeyfileKey(ordered bool, keyfiles []io.Reader) ([32]byte, error) { - if len(keyfiles) == 0 { - return [32]byte{}, nil - } - - hashes := make([][32]byte, len(keyfiles)) - hasher := sha3.New256() - for i, file := range keyfiles { - if err := computeHash(hasher, file, hashes[i][:]); err != nil { - return [32]byte{}, err - } - if !ordered { - hasher.Reset() - } - } - - if ordered { - key := [32]byte{} - copy(key[:], hasher.Sum(nil)) - return key, nil - } - - key := [32]byte{} - for _, hash := range hashes { - key = xor(key, hash) - } - if hasDuplicates(hashes) { - return key, ErrDuplicateKeyfiles - } - return key, nil -} - -func hasDuplicates(hashes [][32]byte) bool { - hashSet := make(map[string]struct{}, len(hashes)) - for _, hash := range hashes { - hashStr := string(hash[:]) - if _, exists := hashSet[hashStr]; exists { - return true - } - hashSet[hashStr] = struct{}{} - } - return false -} - -func argon2IDKey(password string, salt [16]byte, iterations uint32, parallelism uint8) [32]byte { - var key [32]byte - copy(key[:], argon2.IDKey([]byte(password), salt[:], iterations, 1<<20, parallelism, 32)) - return key -} - -var argonKey = argon2IDKey - -func generatePasswordKey(password string, salt [16]byte, paranoid bool) [32]byte { - iterations := uint32(4) - parallelism := uint8(4) - if paranoid { - iterations = 8 - parallelism = 8 - } - return argonKey(password, salt, iterations, parallelism) -} - -func generateDenyKey(password string, salt [16]byte) [32]byte { - return argonKey(password, salt, 4, 4) -} - -func newKeys(settings Settings, seeds seeds, password string, keyfiles []io.Reader) (keys, error) { - keyfileKey, err := generateKeyfileKey(settings.OrderedKf, keyfiles) - if err != nil && !errors.Is(err, ErrDuplicateKeyfiles) { - return keys{}, fmt.Errorf("creating keys: %w", err) - } - duplicateKeyfiles := errors.Is(err, ErrDuplicateKeyfiles) - - passwordKey := generatePasswordKey(password, seeds.Salt, settings.Paranoid) - - var keyRef [64]byte - err = computeHash(sha3.New512(), bytes.NewBuffer(passwordKey[:]), keyRef[:]) - if err != nil { - return keys{}, fmt.Errorf("creating keys: %w", err) - } - var keyfileRef [32]byte - if len(keyfiles) > 0 { - computeHash(sha3.New256(), bytes.NewBuffer(keyfileKey[:]), keyfileRef[:]) - } - - key := xor(keyfileKey, passwordKey) - - var denyKey [32]byte - if settings.Deniability { - denyKey = generateDenyKey(password, seeds.DenySalt) - } - - hkdf := hkdf.New(sha3.New256, key[:], seeds.HkdfSalt[:], nil) - var macKey [32]byte - if _, err := io.ReadFull(hkdf, macKey[:]); err != nil { - return keys{}, fmt.Errorf("filling macKey: %w", err) - } - var serpentKey [32]byte - if _, err := io.ReadFull(hkdf, serpentKey[:]); err != nil { - return keys{}, fmt.Errorf("filling serpentKey: %w", err) - } - - keys := keys{ - key: key, - macKey: macKey, - serpentKey: serpentKey, - denyKey: denyKey, - hkdf: hkdf, - keyRef: keyRef, - keyfileRef: keyfileRef, - } - - if duplicateKeyfiles { - return keys, ErrDuplicateKeyfiles - } - return keys, nil -} - -func computeHash(hasher hash.Hash, src io.Reader, dest []byte) error { - data, err := io.ReadAll(src) - if err != nil { - return fmt.Errorf("reading src: %w", err) - } - _, err = hasher.Write(data) - if err != nil { - return fmt.Errorf("hashing src: %w", err) - } - copy(dest, hasher.Sum(nil)) - return nil -} diff --git a/internal/encryption/keys_test.go b/internal/encryption/keys_test.go deleted file mode 100644 index 288012d..0000000 --- a/internal/encryption/keys_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package encryption - -import ( - "bytes" - "crypto/rand" - "errors" - "io" - "testing" -) - -func TestXor(t *testing.T) { - zeros := [32]byte{} - ones := [32]byte{} - for i := 0; i < len(ones); i++ { - ones[i] = 255 - } - - result := xor(zeros, zeros) - if !bytes.Equal(zeros[:], result[:]) { - t.Fatal("xor zeros with zeros should be zero") - } - - result = xor(ones, ones) - if !bytes.Equal(zeros[:], result[:]) { - t.Fatal("xor ones with ones should be zero") - } - - result = xor(zeros, ones) - if !bytes.Equal(ones[:], result[:]) { - t.Fatal("xor zeros with ones should be ones") - } - - result = xor(ones, zeros) - if !bytes.Equal(ones[:], result[:]) { - t.Fatal("xor ones with zeros should be zero") - } -} - -func TestGenKeyfileKeyOrdered(t *testing.T) { - kf1 := make([]byte, 100) - rand.Read(kf1) - kf2 := make([]byte, 200) - rand.Read(kf2) - - kf12 := []io.Reader{bytes.NewBuffer(kf1), bytes.NewBuffer(kf2)} - key12, err := generateKeyfileKey(true, kf12) - if err != nil { - t.Fatal("generating keyfile key:", err) - } - - kf21 := []io.Reader{bytes.NewBuffer(kf2), bytes.NewBuffer(kf1)} - key21, err := generateKeyfileKey(true, kf21) - if err != nil { - t.Fatal("generating keyfile key:", err) - } - - if bytes.Equal(key12[:], key21[:]) { - t.Fatal("key order should change result") - } -} - -func TestGenKeyfileKeyUnordered(t *testing.T) { - kf1 := make([]byte, 100) - rand.Read(kf1) - kf2 := make([]byte, 200) - rand.Read(kf2) - - kf12 := []io.Reader{bytes.NewBuffer(kf1), bytes.NewBuffer(kf2)} - key12, err := generateKeyfileKey(false, kf12) - if err != nil { - t.Fatal("generating keyfile key:", err) - } - - kf21 := []io.Reader{bytes.NewBuffer(kf2), bytes.NewBuffer(kf1)} - key21, err := generateKeyfileKey(false, kf21) - if err != nil { - t.Fatal("generating keyfile key:", err) - } - - if !bytes.Equal(key12[:], key21[:]) { - t.Fatal("key order should not change result") - } -} - -func TestGenKeyfileKeyDuplicated(t *testing.T) { - kf1 := make([]byte, 100) - kf2 := make([]byte, 100) - kf3 := make([]byte, 100) - rand.Read(kf1) - copy(kf2[:], kf1[:]) - rand.Read(kf3) - key, err := generateKeyfileKey( - false, - []io.Reader{bytes.NewBuffer(kf1), bytes.NewBuffer(kf2), bytes.NewBuffer(kf3)}, - ) - if !errors.Is(err, ErrDuplicateKeyfiles) { - t.Fatal("expected duplicate keyfile error") - } - if bytes.Equal(key[:], make([]byte, 32)) { - t.Fatal("key should still be generated") - } -} diff --git a/internal/encryption/picocrypt_compatibility_test.go b/internal/encryption/picocrypt_compatibility_test.go deleted file mode 100644 index 470af6d..0000000 --- a/internal/encryption/picocrypt_compatibility_test.go +++ /dev/null @@ -1,357 +0,0 @@ -// Test for combatibility with Picocrypt -package encryption - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "hash" - "io" - "log" - "os" - "strconv" - "strings" - "testing" -) - -var compatibilityVersions = []string{"v1.47", "v1.48"} - -/* Decrypt files encrypted by Picocrypt - -To generate the test files, choose a version of Picocrypt and rotate through each file under -picocrypt_samples/basefiles. The file sizes tested are: -- 0 bytes. While encrypted empyt files is not really meaningful, encryption should still work -- 1000 bytes. Just a generic small file of data -- 1048570 bytes. This should trigger the extra flag for Reed-Solomon encoding that Picocrypt uses - -Encrypt each file with password "password". Append the following strings to the filename to indicate -what other encryption settings were used. Note that only the keyfile settings are actually used in -the test because the others can be derived from the header data. The non-keyfile suffixes are just -for reference to check what combinations of settings are being tested. -- "_c" indicates that comments were used -- "_p" indicates that paranoid mode was used -- "_r" indicates that Reed-Solomon encoding was used -- "_d" indicates that deniability mode was used -- "_kf1" indicates that keyfile 1 was used -- "_kf12" indicates that keyfiles 1 and 2 were used -- "_kf12o" indicates that keyfiles 1 and 2 were used, and the keyfiles were ordered - -The encrypted files should be stored under picocrypt_samples// -*/ - -func getTestKeyfiles(name string) []io.Reader { - kf := []io.Reader{} - usesKf1 := strings.Contains(name, "kf1") - usesKf2 := strings.Contains(name, "kf12") - for i, used := range []bool{usesKf1, usesKf2} { - if !used { - continue - } - r, err := os.Open("picocrypt_samples/keyfiles/keyfile" + strconv.Itoa(i+1)) - if err != nil { - log.Fatal("opening keyfile: %w", err) - } - defer r.Close() - buf := bytes.NewBuffer([]byte{}) - _, err = io.Copy(buf, r) - if err != nil { - log.Fatal("reading keyfile: %w", err) - } - kf = append(kf, buf) - } - return kf -} - -func getBaseData(name string) []byte { - basename := strings.SplitN(name, ".", 2)[0] - if strings.Contains(basename, "_") { - basename = strings.SplitN(basename, "_", 2)[0] - } - r, err := os.Open("picocrypt_samples/basefiles/" + basename) - if err != nil { - log.Fatal("opening file: %w", err) - } - defer r.Close() - buf := bytes.NewBuffer([]byte{}) - _, err = io.Copy(buf, r) - if err != nil { - log.Fatal("reading file: %w", err) - } - return buf.Bytes() -} - -func TestDecryptingPicocryptFiles(t *testing.T) { - for _, version := range compatibilityVersions { - files, err := os.ReadDir("picocrypt_samples/" + version) - if err != nil { - t.Fatal("reading directory: %w", err) - } - for _, file := range files { - if !(strings.HasSuffix(file.Name(), ".pcv")) { - continue - } - t.Run(version+":"+file.Name(), func(t *testing.T) { - r, err := os.Open("picocrypt_samples/" + version + "/" + file.Name()) - if err != nil { - t.Fatal("opening encrypted file: %w", err) - } - defer r.Close() - w := bytes.NewBuffer([]byte{}) - kf := getTestKeyfiles(file.Name()) - damaged, err := Decrypt("password", kf, r, w, false) - if damaged { - t.Fatal("damaged data") - } - if err != nil { - t.Fatal("decrypting:", err) - } - result := w.Bytes() - expected := getBaseData(file.Name()) - if !bytes.Equal(result, expected) { - t.Fatal("decrypted data does not match") - } - }) - } - } -} - -/* Test encryption parity with Picocrypt - -Given the same header (seeds, settings, etc), same password, and same keyfiles, the encryption -package should produce exactly the same output as Picocrypt. Run through each file in the -latest version and ensure that the encrypted data matches exactly. -*/ - -func extractSettings(path string) (Settings, seeds) { - r, err := os.Open(path) - if err != nil { - log.Fatal("opening file: %w", err) - } - defer r.Close() - header, err := getHeader(r, "password") - if err != nil { - log.Fatal("reading header: %w", err) - } - return header.settings, header.seeds -} - -func TestEncryptedFilesMatchPicocrypt(t *testing.T) { - latestVersion := compatibilityVersions[len(compatibilityVersions)-1] - files, err := os.ReadDir("picocrypt_samples/" + latestVersion) - if err != nil { - t.Fatal("reading directory: %w", err) - } - for _, file := range files { - if !(strings.HasSuffix(file.Name(), ".pcv")) { - continue - } - t.Run("encrypting:"+file.Name(), func(t *testing.T) { - path := "picocrypt_samples/" + latestVersion + "/" + file.Name() - settings, seeds := extractSettings(path) - kf := getTestKeyfiles(file.Name()) - w := bytes.NewBuffer([]byte{}) - header, err := encryptWithSeeds( - bytes.NewBuffer(getBaseData(file.Name())), - "password", - kf, - settings, - w, - seeds, - ) - if err != nil { - t.Fatal("encrypting:", err) - } - full := bytes.NewBuffer([]byte{}) - err = PrependHeader(bytes.NewBuffer(w.Bytes()), full, header) - if err != nil { - t.Fatal("prepending header:", err) - } - result := full.Bytes() - - r, err := os.Open(path) - if err != nil { - t.Fatal("opening encrypted file: %w", err) - } - defer r.Close() - buf := bytes.NewBuffer([]byte{}) - _, err = io.Copy(buf, r) - if err != nil { - t.Fatal("reading file: %w", err) - } - expected := buf.Bytes() - - if !bytes.Equal(result, expected) { - t.Fatal("encrypted data does not match") - } - }) - } -} - -/* Compare encryption and decryption for very large files - -The encryption and decryption methods rotate the nonces every 60 GB. To accurately test this, -encrypt/decrypt files larger than 60 GB. Instead of adding files that large to the repository, -create a test that simulates this behavior. These tests will generate files of zeros, encrypt -them, and then decrypt them. The sha256 of the encrypted and decrypted files are compared against -expected values generated by hand, following these steps: - -1. Create a file of zeros (bash ex: dd if=/dev/zero of=zerofile bs=1M count=62464) -2. Get the sha256 of the file (bash ex: sha256sum zerofile) -3. Encrypt the file using the latest Picocrypt version. This is a long test, so only check against - one version -4. Get the sha256sum of the encrypted file (bash ex: sha256sum zerofile.pcv) -5. Save the header bytes from the encrypted file (bash ex: head -c 789 zerofile.pcv > zerofile.header). -6. Create a test with the header bytes and the sha256sums -*/ - -type zeroReader struct { - size int64 - counter int64 -} - -func (z *zeroReader) Read(p []byte) (n int, err error) { - if z.counter == z.size { - return 0, io.EOF - } - for i := range p { - p[i] = 0 - z.counter++ - if z.counter == z.size { - return i + 1, nil - } - } - return len(p), nil -} - -type shaDecryptWriter struct { - decryptStream *decryptStream - encryptedSha hash.Hash - decryptedSha hash.Hash -} - -func (s *shaDecryptWriter) Write(p []byte) (int, error) { - _, err := s.encryptedSha.Write(p) - if err != nil { - return 0, err - } - decoded, err := s.decryptStream.stream(p) - if err != nil { - return 0, err - } - _, err = s.decryptedSha.Write(decoded) - if err != nil { - return 0, err - } - return len(p), nil -} - -func (s *shaDecryptWriter) shas() ([]byte, []byte, error) { - decoded, err := s.decryptStream.flush() - if err != nil { - return nil, nil, err - } - _, err = s.decryptedSha.Write(decoded) - if err != nil { - return nil, nil, err - } - return s.encryptedSha.Sum(nil), s.decryptedSha.Sum(nil), nil -} - -func compareShas( - t *testing.T, - password string, - headerFilename string, - encodedSha string, - decodedSha string, - zeroFileSize int64, -) { - headerReader, err := os.Open(headerFilename) - if err != nil { - t.Fatal("opening header file:", err) - } - defer headerReader.Close() - headerBytes, err := io.ReadAll(headerReader) - if err != nil { - t.Fatal("reading header:", err) - } - - damageTracker := &damageTracker{} - writer := &shaDecryptWriter{makeDecryptStream(password, nil, damageTracker), sha256.New(), sha256.New()} - _, err = writer.Write(headerBytes) - if err != nil { - t.Fatal("writing header:", err) - } - if !writer.decryptStream.headerStream.isDone() { - t.Fatal("header stream should be done") - } - - _, err = encryptWithSeeds( - &zeroReader{size: zeroFileSize}, - password, - []io.Reader{}, - writer.decryptStream.headerStream.header.settings, - writer, - writer.decryptStream.headerStream.header.seeds, - ) - if err != nil { - t.Fatal("encrypting:", err) - } - - eSha, dSha, err := writer.shas() - if err != nil { - t.Fatal("getting shas:", err) - } - if hex.EncodeToString(eSha) != encodedSha { - t.Fatal("encoded sha256 does not match") - } - if hex.EncodeToString(dSha) != decodedSha { - t.Fatal("decoded sha256 does not match") - } -} - -func isLatestVersion(path string) bool { - r, err := os.Open(path) - if err != nil { - log.Fatal("opening file: %w", err) - } - defer r.Close() - expected := compatibilityVersions[len(compatibilityVersions)-1] - buf := make([]byte, len(expected)) - _, err = io.ReadFull(r, buf) - if err != nil { - log.Fatal("reading file: %w", err) - } - return string(buf) == expected -} - -func TestSmallFileSha(t *testing.T) { - // Test 1K file of zeros, for debugging - path := "picocrypt_samples/headers/zerofile1024.header" - if !isLatestVersion(path) { - t.Fatal("header file is not latest version") - } - compareShas( - t, - "password", - path, - "bb8a1f6be9cfcd39cb8eedd49583ddfd7153158b8ffce541e256deb400160a98", - "5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", - (1 << 10), // 1K - ) -} - -func TestLargeFileSha(t *testing.T) { - // Test 65GB file of zeros - path := "picocrypt_samples/headers/zerofile65498251264.header" - if !isLatestVersion(path) { - t.Fatal("header file is not latest version") - } - compareShas( - t, - "password", - path, - "451dd75500b9502e7c82fa679e698443337d33c52ddb84d5a14c3ff95032dc50", - "f3f0d678fa138e4581ed15ec63f8cb965e5d7b722db7d5fc4877e763163d399c", - 65498251264, - ) -} diff --git a/internal/encryption/picocrypt_samples/basefiles/base0 b/internal/encryption/picocrypt_samples/basefiles/base0 deleted file mode 100644 index e69de29..0000000 diff --git a/internal/encryption/picocrypt_samples/basefiles/base1000 b/internal/encryption/picocrypt_samples/basefiles/base1000 deleted file mode 100644 index 23bf4d1..0000000 Binary files a/internal/encryption/picocrypt_samples/basefiles/base1000 and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/basefiles/base1048570 b/internal/encryption/picocrypt_samples/basefiles/base1048570 deleted file mode 100644 index fcdcf72..0000000 Binary files a/internal/encryption/picocrypt_samples/basefiles/base1048570 and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/headers/zerofile1024.header b/internal/encryption/picocrypt_samples/headers/zerofile1024.header deleted file mode 100644 index 1cb9f4a..0000000 Binary files a/internal/encryption/picocrypt_samples/headers/zerofile1024.header and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/headers/zerofile65498251264.header b/internal/encryption/picocrypt_samples/headers/zerofile65498251264.header deleted file mode 100644 index 46685eb..0000000 Binary files a/internal/encryption/picocrypt_samples/headers/zerofile65498251264.header and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/keyfiles/keyfile1 b/internal/encryption/picocrypt_samples/keyfiles/keyfile1 deleted file mode 100644 index 0ded3bc..0000000 --- a/internal/encryption/picocrypt_samples/keyfiles/keyfile1 +++ /dev/null @@ -1,2 +0,0 @@ -y»_œòÒi<2üL¢ëîtÀ¥ -©/¹g ÎàíUü}à \ No newline at end of file diff --git a/internal/encryption/picocrypt_samples/keyfiles/keyfile2 b/internal/encryption/picocrypt_samples/keyfiles/keyfile2 deleted file mode 100644 index 0e00624..0000000 --- a/internal/encryption/picocrypt_samples/keyfiles/keyfile2 +++ /dev/null @@ -1 +0,0 @@ -—¡ÀÛ¦‘°a÷vyò“RÄ@Hÿ½ðR©tø»jØ \ No newline at end of file diff --git a/internal/encryption/picocrypt_samples/v1.47/base0.pcv b/internal/encryption/picocrypt_samples/v1.47/base0.pcv deleted file mode 100644 index 222634f..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base0_c.pcv b/internal/encryption/picocrypt_samples/v1.47/base0_c.pcv deleted file mode 100644 index 8974a4b..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0_c.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base0_d.pcv b/internal/encryption/picocrypt_samples/v1.47/base0_d.pcv deleted file mode 100644 index 5b1db93..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0_d.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base0_kf1.pcv b/internal/encryption/picocrypt_samples/v1.47/base0_kf1.pcv deleted file mode 100644 index 8b10b86..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0_kf1.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base0_kf12.pcv b/internal/encryption/picocrypt_samples/v1.47/base0_kf12.pcv deleted file mode 100644 index ee7132e..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0_kf12.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base0_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.47/base0_kf12o.pcv deleted file mode 100644 index ec18ff5..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base0_p.pcv b/internal/encryption/picocrypt_samples/v1.47/base0_p.pcv deleted file mode 100644 index 9285d76..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0_p.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base0_p_r_d_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.47/base0_p_r_d_kf12o.pcv deleted file mode 100644 index 78c5c42..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0_p_r_d_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base0_r.pcv b/internal/encryption/picocrypt_samples/v1.47/base0_r.pcv deleted file mode 100644 index fab7831..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base0_r.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000.pcv deleted file mode 100644 index 40ba8bc..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000_c.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000_c.pcv deleted file mode 100644 index 7d5b20c..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000_c.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000_c_p_r_d_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000_c_p_r_d_kf12o.pcv deleted file mode 100644 index 3eb13dd..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000_c_p_r_d_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000_d.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000_d.pcv deleted file mode 100644 index e3fb60a..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000_d.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000_kf1.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000_kf1.pcv deleted file mode 100644 index dc876cc..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000_kf1.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000_kf12.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000_kf12.pcv deleted file mode 100644 index 99e3bdd..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000_kf12.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000_kf12o.pcv deleted file mode 100644 index ba356d4..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000_p.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000_p.pcv deleted file mode 100644 index f28ee95..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000_p.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1000_r.pcv b/internal/encryption/picocrypt_samples/v1.47/base1000_r.pcv deleted file mode 100644 index 828b2e0..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1000_r.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570.pcv deleted file mode 100644 index 0568791..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570_c.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570_c.pcv deleted file mode 100644 index 35adce8..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570_c.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570_d.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570_d.pcv deleted file mode 100644 index d3d963c..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570_d.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570_kf1.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570_kf1.pcv deleted file mode 100644 index 5f5f13c..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570_kf1.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570_kf12.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570_kf12.pcv deleted file mode 100644 index dce9dae..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570_kf12.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570_kf12o.pcv deleted file mode 100644 index 33b235d..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570_p.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570_p.pcv deleted file mode 100644 index b6cca44..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570_p.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570_p_r_d_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570_p_r_d_kf12o.pcv deleted file mode 100644 index e5027f2..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570_p_r_d_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.47/base1048570_r.pcv b/internal/encryption/picocrypt_samples/v1.47/base1048570_r.pcv deleted file mode 100644 index 731967f..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.47/base1048570_r.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0.pcv b/internal/encryption/picocrypt_samples/v1.48/base0.pcv deleted file mode 100644 index 070fb5f..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0_c.pcv b/internal/encryption/picocrypt_samples/v1.48/base0_c.pcv deleted file mode 100644 index 06f8368..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0_c.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0_d.pcv b/internal/encryption/picocrypt_samples/v1.48/base0_d.pcv deleted file mode 100644 index f69a187..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0_d.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0_kf1.pcv b/internal/encryption/picocrypt_samples/v1.48/base0_kf1.pcv deleted file mode 100644 index 3cb1c32..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0_kf1.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0_kf12.pcv b/internal/encryption/picocrypt_samples/v1.48/base0_kf12.pcv deleted file mode 100644 index b3effa7..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0_kf12.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.48/base0_kf12o.pcv deleted file mode 100644 index ee45ed8..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0_p.pcv b/internal/encryption/picocrypt_samples/v1.48/base0_p.pcv deleted file mode 100644 index 96eea27..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0_p.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0_p_r_d_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.48/base0_p_r_d_kf12o.pcv deleted file mode 100644 index 9a1e4c8..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0_p_r_d_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base0_r.pcv b/internal/encryption/picocrypt_samples/v1.48/base0_r.pcv deleted file mode 100644 index 7436238..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base0_r.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000.pcv deleted file mode 100644 index ed98ee9..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000_c.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000_c.pcv deleted file mode 100644 index e6e666a..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000_c.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000_d.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000_d.pcv deleted file mode 100644 index 73e0176..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000_d.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000_kf1.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000_kf1.pcv deleted file mode 100644 index 4fc1f41..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000_kf1.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000_kf12.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000_kf12.pcv deleted file mode 100644 index 954780b..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000_kf12.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000_kf12o.pcv deleted file mode 100644 index 875be8f..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000_p.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000_p.pcv deleted file mode 100644 index 266017f..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000_p.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000_p_r_d_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000_p_r_d_kf12o.pcv deleted file mode 100644 index e887495..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000_p_r_d_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1000_r.pcv b/internal/encryption/picocrypt_samples/v1.48/base1000_r.pcv deleted file mode 100644 index 0464fb7..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1000_r.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570.pcv deleted file mode 100644 index 4be8049..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570_c.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570_c.pcv deleted file mode 100644 index ef333f8..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570_c.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570_d.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570_d.pcv deleted file mode 100644 index f17fe97..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570_d.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570_kf1.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570_kf1.pcv deleted file mode 100644 index c588cc3..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570_kf1.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570_kf12.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570_kf12.pcv deleted file mode 100644 index 8b97e97..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570_kf12.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570_kf12o.pcv deleted file mode 100644 index a8f74cb..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570_p.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570_p.pcv deleted file mode 100644 index 5cb0674..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570_p.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570_p_r_d_kf12o.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570_p_r_d_kf12o.pcv deleted file mode 100644 index 704613a..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570_p_r_d_kf12o.pcv and /dev/null differ diff --git a/internal/encryption/picocrypt_samples/v1.48/base1048570_r.pcv b/internal/encryption/picocrypt_samples/v1.48/base1048570_r.pcv deleted file mode 100644 index 14206a3..0000000 Binary files a/internal/encryption/picocrypt_samples/v1.48/base1048570_r.pcv and /dev/null differ diff --git a/internal/encryption/reed_solomon.go b/internal/encryption/reed_solomon.go deleted file mode 100644 index b640bc0..0000000 --- a/internal/encryption/reed_solomon.go +++ /dev/null @@ -1,186 +0,0 @@ -package encryption - -import ( - "bytes" - "fmt" - "sync" - - "github.com/Picocrypt/infectious" -) - -const ( - chunkSize = 128 - encodedSize = 136 -) - -var ( - fecs = make(map[[2]int]*infectious.FEC) - fecsMu sync.Mutex -) - -func getFEC(encoded, decoded []byte) (*infectious.FEC, error) { - size := [2]int{len(decoded), len(encoded)} - - fecsMu.Lock() - defer fecsMu.Unlock() - - fec := fecs[size] - if fec != nil { - return fec, nil - } - - fec, err := infectious.NewFEC(size[0], size[1]) - if err != nil { - return fec, err - } - - fecs[size] = fec - return fec, nil -} - -func rsEncode(dst, src []byte) error { - fec, err := getFEC(dst, src) - if err != nil { - return fmt.Errorf("getting FEC: %w", err) - } - return fec.Encode(src, func(s infectious.Share) { dst[s.Number] = s.Data[0] }) -} - -func rsDecode(dst, src []byte, skip bool) (bool, bool, error) { - if skip { - copy(dst, src[:len(dst)]) - return false, false, nil - } - - // Encoding is much faster than decoding. Try re-encoding the original - // bytes and if the result matches, there must have been no corruption. - recoded := make([]byte, len(src)) - rsEncode(recoded, src[:len(dst)]) - if bytes.Equal(recoded, src) { - copy(dst, src[:len(dst)]) - return false, false, nil - } - - // Attempt to recover damaged data - fec, err := getFEC(src, dst) - if err != nil { - return true, false, fmt.Errorf("getting FEC: %w", err) - } - tmp := make([]infectious.Share, fec.Total()) - for i := 0; i < fec.Total(); i++ { - tmp[i].Number = i - tmp[i].Data = []byte{src[i]} - } - res, err := fec.Decode(nil, tmp) - if err == nil { - copy(dst, res) - return true, false, nil - } - - // Fully corrupted - use a best guess - copy(dst, src[:len(dst)]) - return true, true, nil -} - -type rsEncodeStream struct { - buff []byte - chunksEncoded int64 - header *header -} - -func (r *rsEncodeStream) stream(p []byte) ([]byte, error) { - r.buff = append(r.buff, p...) - nChunks := len(r.buff) / chunkSize - rsData := make([]byte, nChunks*encodedSize) - for i := 0; i < nChunks; i++ { - err := rsEncode(rsData[i*encodedSize:(i+1)*encodedSize], r.buff[i*chunkSize:(i+1)*chunkSize]) - if err != nil { - return nil, err - } - } - r.buff = r.buff[nChunks*chunkSize:] - r.chunksEncoded += int64(nChunks) - return rsData, nil -} - -func (r *rsEncodeStream) flush() ([]byte, error) { - // Skip if exactly the right number of chunks encoded and there is no buffer data - exactMiB := r.chunksEncoded%((1<<20)/chunkSize) == 0 - if exactMiB && len(r.buff) == 0 { - return nil, nil - } - padding := make([]byte, chunkSize-len(r.buff)) - for i := range padding { - padding[i] = byte(chunkSize - len(r.buff)) - } - dst := make([]byte, encodedSize) - err := rsEncode(dst, append(r.buff, padding...)) - if err != nil { - return nil, fmt.Errorf("encoding final chunk: %w", err) - } - return dst, nil -} - -type rsDecodeStream struct { - buff []byte - skip bool - damageTracker *damageTracker - header *header - chunksDecoded int64 -} - -func (r *rsDecodeStream) stream(p []byte) ([]byte, error) { - r.buff = append(r.buff, p...) - nChunks := len(r.buff) / encodedSize - // The last chunk might be padded, so keep it in the buffer for flush - if ((len(r.buff) % encodedSize) == 0) && (nChunks > 0) { - nChunks -= 1 - } - rsData := make([]byte, nChunks*chunkSize) - for i := 0; i < nChunks; i++ { - src := r.buff[i*encodedSize : (i+1)*encodedSize] - dst := rsData[i*chunkSize : (i+1)*chunkSize] - damaged, _, err := rsDecode(dst, src, r.skip) - r.damageTracker.damage = r.damageTracker.damage || damaged - if err != nil { - return nil, err - } - } - r.chunksDecoded += int64(nChunks) - r.buff = r.buff[nChunks*encodedSize:] - return rsData, nil -} - -func (r *rsDecodeStream) flush() ([]byte, error) { - if len(r.buff) == 0 { - return nil, nil - } - if len(r.buff) != encodedSize { - return nil, ErrBodyCorrupted - } - res := make([]byte, chunkSize) - damaged, _, err := rsDecode(res, r.buff, r.skip) - r.damageTracker.damage = r.damageTracker.damage || damaged - if err != nil { - return nil, err - } - // The last chunk is padded unless there is an exact multiple of MiB of decoded data - // and the nearMiBFlag is not set - exactMiB := (r.chunksDecoded+1)%((1<<20)/chunkSize) == 0 - if exactMiB && !r.header.nearMiBFlag { - return res, nil - } - keep := chunkSize - int(res[chunkSize-1]) - if (keep >= 0) && (keep < chunkSize) { - return res[:keep], err - } - return nil, ErrBodyCorrupted -} - -func makeRSEncodeStream(header *header) *rsEncodeStream { - return &rsEncodeStream{header: header} -} - -func makeRSDecodeStream(skip bool, header *header, damageTracker *damageTracker) *rsDecodeStream { - return &rsDecodeStream{skip: skip, damageTracker: damageTracker, header: header} -} diff --git a/internal/encryption/stream.go b/internal/encryption/stream.go deleted file mode 100644 index b86b228..0000000 --- a/internal/encryption/stream.go +++ /dev/null @@ -1,40 +0,0 @@ -package encryption - -type streamer interface { - stream(p []byte) ([]byte, error) -} - -type streamerFlusher interface { - streamer - flush() ([]byte, error) -} - -func streamStack[T streamer](streams []T, p []byte) ([]byte, error) { - var err error - for _, stream := range streams { - p, err = stream.stream(p) - if err != nil { - return nil, err - } - if len(p) == 0 { - break - } - } - return p, nil -} - -func flushStack(streams []streamerFlusher) ([]byte, error) { - p := []byte{} - for _, stream := range streams { - pStream, err := stream.stream(p) - if err != nil { - return nil, err - } - pFlush, err := stream.flush() - if err != nil { - return nil, err - } - p = append(pStream, pFlush...) - } - return p, nil -} diff --git a/internal/ui/elements.go b/internal/ui/elements.go deleted file mode 100644 index e7aac48..0000000 --- a/internal/ui/elements.go +++ /dev/null @@ -1,291 +0,0 @@ -package ui - -import ( - "crypto/rand" - "errors" - "fmt" - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/theme" - "fyne.io/fyne/v2/widget" -) - -func MakeInfoBtn(w fyne.Window) *widget.Button { // coverage-ignore - btn := widget.NewButtonWithIcon("", theme.InfoIcon(), func() { - title := "PicoGo " + PicoGoVersion - message := "This app is not sponsored or supported by Picocrypt. It is a 3rd party " + - "app written to make Picocrypt files more easily accessible on mobile devices.\n\n" + - "If you have any problems, please report them so that they can be fixed." - confirm := dialog.NewInformation(title, message, w) - confirm.Show() - }) - return btn -} - -func writeLogsCallback(logger *Logger, window fyne.Window) func(fyne.URIWriteCloser, error) { - return func(writer fyne.URIWriteCloser, err error) { - if writer != nil { - defer writer.Close() - } - if err != nil { // coverage-ignore - logger.Log("Writing logs failed", State{}, err) - dialog.ShowError(fmt.Errorf("writing logs: %w", err), window) - return - } - if writer != nil { - writer.Write([]byte(logger.CsvString())) - } - } -} - -func writeLogs(logger *Logger, window fyne.Window) { // coverage-ignore - d := dialog.NewFileSave(writeLogsCallback(logger, window), window) - d.SetFileName("picogo-logs.csv") - d.Show() -} - -func MakeLogBtn(logger *Logger, w fyne.Window) *widget.Button { // coverage-ignore - btn := widget.NewButtonWithIcon("", theme.MailSendIcon(), func() { - title := "Save Logs" - message := "Save log data to assist with issue reporting. Sensitive data (passwords, file names, etc.) " + - "will not be recorded, but you should still review the logs before sharing to ensure you are " + - "comfortable with the data being shared." - text := widget.NewLabel(message) - text.Wrapping = fyne.TextWrapWord - dialog.ShowCustomConfirm(title, "Save Logs", "Dismiss", text, func(b bool) { - if b { - writeLogs(logger, w) - } - }, w) - }) - return btn -} - -func MakeSettingsBtn(settings *Settings, parent fyne.Window) *widget.Button { // coverage-ignore - btn := widget.NewButtonWithIcon("", theme.SettingsIcon(), func() { - dialog.ShowCustom( - "Default Settings", - "Close", - container.New( - layout.NewVBoxLayout(), - settings.ReedSolomonDefault, - settings.ParanoidDefault, - settings.DeniabilityDefault, - settings.OrderedKfDefault, - ), - parent, - ) - }) - return btn -} - -func filePickerCallback(state *State, logger *Logger, w fyne.Window) func(fyne.URIReadCloser, error) { - return func(reader fyne.URIReadCloser, err error) { - if reader != nil { - defer reader.Close() - } - if err != nil { // coverage-ignore - logger.Log("Choosing file to encrypt/decrypt failed", *state, err) - dialog.ShowError(fmt.Errorf("choosing file: %w", err), w) - return - } - if reader == nil { - logger.Log("Choosing file to encrypt/decrypt failed", *state, errors.New("no file chosen")) - return - } - err = state.SetInput(reader.URI()) - logger.Log("Setting file to encrypt/decrypt", *state, err) - if err != nil { // coverage-ignore - dialog.ShowError(fmt.Errorf("choosing file: %w", err), w) - } - } -} - -func MakeFilePicker(state *State, logger *Logger, w fyne.Window) *widget.Button { // coverage-ignore - picker := widget.NewButtonWithIcon("Choose File", theme.FileIcon(), func() { - fd := dialog.NewFileOpen(filePickerCallback(state, logger, w), w) - fd.Show() - }) - return picker -} - -func makeComments() *widget.Entry { - comments := widget.NewMultiLineEntry() - comments.Validator = nil - comments.Wrapping = fyne.TextWrapWord - return comments -} - -func keyfileText() *widget.Entry { // coverage-ignore - text := widget.NewMultiLineEntry() - text.Disable() - text.SetPlaceHolder("No keyfiles added") - return text -} - -func keyfileAddCallback(state *State, logger *Logger, window fyne.Window) func(fyne.URIReadCloser, error) { - return func(reader fyne.URIReadCloser, err error) { - if reader != nil { - defer reader.Close() - } - if err != nil { // coverage-ignore - logger.Log("Adding keyfile failed", *state, err) - dialog.ShowError(fmt.Errorf("adding keyfile: %w", err), window) - return - } - if reader != nil { - state.AddKeyfile(reader.URI()) - logger.Log("Adding keyfile complete", *state, nil) - } else { - logger.Log("Adding keyfile canceled", *state, nil) - } - } -} - -func KeyfileAddBtn(state *State, logger *Logger, window fyne.Window) *widget.Button { // coverage-ignore - btn := widget.NewButtonWithIcon("Add", theme.ContentAddIcon(), func() { - fd := dialog.NewFileOpen(keyfileAddCallback(state, logger, window), window) - fd.Show() - }) - return btn -} - -func keyfileCreateCallback(state *State, logger *Logger, window fyne.Window) func(fyne.URIWriteCloser, error) { - return func(writer fyne.URIWriteCloser, err error) { - if writer != nil { - defer writer.Close() - } - if err != nil { // coverage-ignore - logger.Log("Creating keyfile failed", *state, err) - dialog.ShowError(fmt.Errorf("creating keyfile: %w", err), window) - return - } - if writer != nil { - data := make([]byte, 32) - _, err := rand.Read(data) - if err != nil { // coverage-ignore - logger.Log("Creating keyfile data failed", *state, err) - dialog.ShowError(fmt.Errorf("creating keyfile: %w", err), window) - return - } - _, err = writer.Write(data) - if err != nil { // coverage-ignore - logger.Log("Writing keyfile failed", *state, err) - dialog.ShowError(fmt.Errorf("writing keyfile: %w", err), window) - return - } - state.AddKeyfile(writer.URI()) - logger.Log("Created keyfile", *state, nil) - } else { - logger.Log("Creating keyfile canceled", *state, nil) - } - } -} - -func KeyfileCreateBtn(state *State, logger *Logger, window fyne.Window) *widget.Button { // coverage-ignore - btn := widget.NewButtonWithIcon("Create", theme.ContentAddIcon(), func() { - fd := dialog.NewFileSave(keyfileCreateCallback(state, logger, window), window) - fd.SetFileName("Keyfile") - fd.Show() - }) - return btn -} - -func keyfileClearCallback(state *State, logger *Logger) func() { - return func() { - logger.Log("Clearing keyfiles", *state, nil) - state.ClearKeyfiles() - } -} - -func KeyfileClearBtn(state *State, logger *Logger) *widget.Button { // coverage-ignore - btn := widget.NewButtonWithIcon("Clear", theme.ContentClearIcon(), keyfileClearCallback(state, logger)) - return btn -} - -func makePassword() *widget.Entry { - password := widget.NewPasswordEntry() - password.SetPlaceHolder("Password") - password.Validator = nil - return password -} - -func makeConfirmPassword() *widget.Entry { - confirm := widget.NewPasswordEntry() - confirm.SetPlaceHolder("Confirm password") - confirm.Validator = nil - return confirm -} - -func WorkBtnCallback(state *State, logger *Logger, w fyne.Window, encrypt func(), decrypt func()) func() { - return func() { - if !(state.IsEncrypting() || state.IsDecrypting()) { - // This should never happen (the button should be hidden), but check in case - // there is a race condition - logger.Log("Encrypt/Decrypt button pressed", *state, errors.New("button should be hidden")) - dialog.ShowError(errors.New("no file chosen"), w) - return - } - if state.IsEncrypting() { - if state.Password.Text != state.ConfirmPassword.Text { - logger.Log("Encrypt/Decrypt button pressed", *state, errors.New("passwords do not match")) - dialog.ShowError(errors.New("passwords do not match"), w) - } else if state.Password.Text == "" { - logger.Log("Encrypt/Decrypt button pressed", *state, errors.New("password cannot be blank")) - dialog.ShowError(errors.New("password cannot be blank"), w) - } else { - logger.Log("Encrypt/Decrypt button pressed (encrypting)", *state, nil) - encrypt() - } - return - } - logger.Log("Encrypt/Decrypt button pressed (decrypting)", *state, nil) - decrypt() - } -} - -type ByteCounter struct { - total int64 - startTime int64 - endTime int64 -} - -func (bc *ByteCounter) Read(p []byte) (int, error) { - bc.total += int64(len(p)) - if bc.startTime == 0 { - bc.startTime = time.Now().Unix() - } - bc.endTime = time.Now().Unix() - return len(p), nil -} - -func (bc *ByteCounter) Write(p []byte) (int, error) { - return bc.Read(p) -} - -func (bc *ByteCounter) stringify(n float64) string { - for i, prefix := range []string{"B", "KB", "MB", "GB"} { - scale := 1 << (10 * i) - if n < float64(scale<<10) { - return fmt.Sprintf("%.2f %s", n/float64(scale), prefix) - } - } - return fmt.Sprintf("%.2f TB", n/(1<<40)) -} - -func (bc *ByteCounter) Total() string { - return bc.stringify(float64(bc.total)) -} - -func (bc *ByteCounter) Rate() string { - elapsed := bc.endTime - bc.startTime - if elapsed == 0 { - return "0 B/s" - } - rate := float64(bc.total) / float64(elapsed) - return bc.stringify(rate) + "/s" -} diff --git a/internal/ui/elements_test.go b/internal/ui/elements_test.go deleted file mode 100644 index cd4f659..0000000 --- a/internal/ui/elements_test.go +++ /dev/null @@ -1,327 +0,0 @@ -package ui - -import ( - "log" - "testing" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/test" -) - -func MakeURI(name string) fyne.URI { - uri, err := storage.ParseURI("file://" + name) - if err != nil { - log.Println("Error creating URI:", err) - panic(err) - } - return uri -} - -type TestReadWriteCloser struct { - bytesWritten int - bytesRead int - isClosed bool -} - -func (t *TestReadWriteCloser) Read(p []byte) (n int, err error) { - t.bytesRead += len(p) - return len(p), nil -} - -func (t *TestReadWriteCloser) Write(p []byte) (n int, err error) { - t.bytesWritten += len(p) - return len(p), nil -} - -func (t *TestReadWriteCloser) Close() error { - t.isClosed = true - return nil -} - -func (t *TestReadWriteCloser) URI() fyne.URI { - return MakeURI("test") -} - -func TestWriteLogsCallback(t *testing.T) { - logger := Logger{} - app := test.NewApp() - window := app.NewWindow("Test Window") - callback := writeLogsCallback(&logger, window) - - test := TestReadWriteCloser{} - callback(&test, nil) - if !test.isClosed { - t.Errorf("Expected TestReadWriteCloser to be closed, but it was not.") - } - if test.bytesWritten == 0 { - t.Errorf("Expected some bytes to be written, but none were.") - } -} - -func TestFilePickerCallback(t *testing.T) { - logger := Logger{} - app := test.NewApp() - window := app.NewWindow("Test Window") - state := NewState(app) - callback := filePickerCallback(state, &logger, window) - test := TestReadWriteCloser{} - - // Test canceling the file picker - if state.Input() != nil { - t.Errorf("State should not be initialized with an input") - } - callback(nil, nil) - if state.Input() != nil { - t.Errorf("State should not be updated with an input") - } - - if state.Input() != nil { - t.Errorf("State should not be initialized with an input") - } - callback(&test, nil) - if !test.isClosed { - t.Errorf("Resource must be closed") - } - if test.bytesRead != 0 { - t.Errorf("Chosen file should not be read yet") - } - if state.Input() == nil { - t.Errorf("State should be updated with an input") - } -} - -func TestFilename(t *testing.T) { - state := NewState(test.NewApp()) - if state.FileName.Text != "" { - t.Errorf("Filename should be empty") - } - - state.SetInput(MakeURI("test-filename")) - if state.FileName.Text != "test-filename" { - t.Errorf("Filname should match input") - } - - state.SetInput(MakeURI("test-filename-2")) - if state.FileName.Text != "test-filename-2" { - t.Errorf("Filname should match input") - } -} - -func TestComments(t *testing.T) { - state := NewState(test.NewApp()) - - state.SetInput(MakeURI("test")) - if !state.IsEncrypting() { - t.Errorf("State should be encrypting") - } - if state.Comments.Text != "" { - t.Errorf("Comments should be empty") - } - if state.Comments.Disabled() { - t.Errorf("Comments should be enabled") - } - if state.Comments.PlaceHolder != "Comments are not encrypted" { - t.Errorf("Comments should warn user that they are not encrypted") - } - - // Choosing deniability should disable comments - state.Deniability.SetChecked(true) - if state.Comments.Text != "" { - t.Errorf("Comments should be empty") - } - if !state.Comments.Disabled() { - t.Errorf("Comments should be disabled") - } - if state.Comments.PlaceHolder != "Comments are disabled in deniability mode" { - t.Errorf("Comments should warn user that they are disabled") - } - if state.Comments.Text != "" { - t.Errorf("State should be updated with comments") - } - - // Switching to decrypting mode should disable comments - err := state.SetInput(MakeURI("test.pcv")) - if err != nil { - t.Errorf("Error setting input: %v", err) - } - if !state.IsDecrypting() { - t.Errorf("State should be decrypting") - } - if state.Comments.Text != "" { - t.Errorf("Comments should be empty") - } - if !state.Comments.Disabled() { - t.Errorf("Comments should be disabled") - } -} - -func TestKeyfileAddCallback(t *testing.T) { - app := test.NewApp() - window := app.NewWindow("Test Window") - state := NewState(app) - logger := Logger{} - callback := keyfileAddCallback(state, &logger, window) - - callback(nil, nil) - reader := TestReadWriteCloser{} - callback(&reader, nil) - if !reader.isClosed { - t.Errorf("Expected TestReadWriteCloser to be closed, but it was not.") - } - if reader.bytesRead != 0 { - t.Errorf("Keyfile should not be read yet") - } - if len(state.Keyfiles) != 1 { - t.Errorf("Expected one keyfile to be added, but got %d", len(state.Keyfiles)) - } -} - -func TestKeyfileCreateCallback(t *testing.T) { - state := NewState(test.NewApp()) - app := test.NewApp() - window := app.NewWindow("Test Window") - logger := Logger{} - callback := keyfileCreateCallback(state, &logger, window) - - callback(nil, nil) - reader := TestReadWriteCloser{} - callback(&reader, nil) - if !reader.isClosed { - t.Errorf("Expected TestReadWriteCloser to be closed, but it was not.") - } - if reader.bytesWritten != 32 { - t.Errorf("Should have written 32 bytes, but wrote %d", reader.bytesWritten) - } - if len(state.Keyfiles) != 1 { - t.Errorf("Expected one keyfile to be added, but got %d", len(state.Keyfiles)) - } -} - -func TestKeyfileClearCallback(t *testing.T) { - state := NewState(test.NewApp()) - logger := Logger{} - callback := keyfileClearCallback(state, &logger) - - state.AddKeyfile(MakeURI("test-keyfile")) - if len(state.Keyfiles) != 1 { - t.Errorf("Expected one keyfile, but got %d", len(state.Keyfiles)) - } - callback() - if len(state.Keyfiles) != 0 { - t.Errorf("Expected no keyfiles, but got %d", len(state.Keyfiles)) - } -} - -func TestKeyfileTextUpdate(t *testing.T) { - state := NewState(test.NewApp()) - if state.KeyfileText.Text != "" { - t.Errorf("Text should be empty") - } - - state.AddKeyfile(MakeURI("test-keyfile-1")) - state.AddKeyfile(MakeURI("test-keyfile-2")) - if state.KeyfileText.Text != "test-keyfile-1\ntest-keyfile-2" { - t.Errorf("Text should be updated to show keyfiles") - } -} - -func TestPasswordEntry(t *testing.T) { - state := NewState(test.NewApp()) - if state.Password.Text != "" { - t.Errorf("Password should be empty") - } - - // Test enabling / disabling - state.SetInput(MakeURI("test.pcv")) - if state.Password.Disabled() { - t.Errorf("Password should be enabled") - } - state.SetInput(MakeURI("test")) - if state.Password.Disabled() { - t.Errorf("Password should be enabled") - } -} - -func TestConfirmEntry(t *testing.T) { - state := NewState(test.NewApp()) - if state.ConfirmPassword.Text != "" { - t.Errorf("Confirm should be empty") - } - - // Test enabling / disabling - state.SetInput(MakeURI("test.pcv")) - if !state.IsDecrypting() { - t.Errorf("State should be decrypting") - } - if !state.ConfirmPassword.Disabled() { - t.Errorf("Confirm should not be enabled for decrypting") - } - state.SetInput(MakeURI("test")) - if !state.IsEncrypting() { - t.Errorf("State should be encrypting") - } - if !state.ConfirmPassword.Visible() { - t.Errorf("Confirm should be visible for encrypting") - } -} - -func TestWorkBtn(t *testing.T) { - logger := Logger{} - app := test.NewApp() - window := app.NewWindow("Test Window") - state := NewState(app) - encryptCalled := false - encrypt := func() { - encryptCalled = true - } - decryptCalled := false - decrypt := func() { - decryptCalled = true - } - state.WorkBtn.OnTapped = WorkBtnCallback(state, &logger, window, encrypt, decrypt) - - // Set state to encrypting - state.SetInput(MakeURI("test")) - if state.WorkBtn.Disabled() { - t.Errorf("Work button should be enabled") - } - if state.WorkBtn.Text != "Encrypt" { - t.Errorf("Work button should say 'Encrypt'") - } - - // Test mismatched passwords - state.Password.Text = "test-password" - state.ConfirmPassword.Text = "test-confirm" - test.Tap(state.WorkBtn) - if encryptCalled || decryptCalled { - t.Errorf("Encrypt or decrypt should not be called") - } - - // Match passwords - state.ConfirmPassword.Text = "test-password" - test.Tap(state.WorkBtn) - if !encryptCalled || decryptCalled { - t.Errorf("Only encrypt should be called") - } - - // Set state to decrypting - err := state.SetInput(MakeURI("test.pcv")) - if err != nil { - t.Errorf("Error setting input: %v", err) - } - if state.WorkBtn.Disabled() { - t.Errorf("Work button should be visible") - } - if state.WorkBtn.Text != "Decrypt" { - t.Errorf("Work button should say 'Decrypt'") - } - - // Test decrypting - encryptCalled = false - decryptCalled = false - test.Tap(state.WorkBtn) - if encryptCalled || !decryptCalled { - t.Errorf("Only decrypt should be called") - } -} diff --git a/internal/ui/logger.go b/internal/ui/logger.go deleted file mode 100644 index 6704aea..0000000 --- a/internal/ui/logger.go +++ /dev/null @@ -1,87 +0,0 @@ -package ui - -import ( - "strconv" - "strings" - "sync" - "time" -) - -func redactedFile(f *fileDesc) string { - if f == nil { - return "null" - } - redactedUri := "parsing-error-" + strconv.Itoa(f.id) - splitIdx := strings.Index(f.uri, ":") - if splitIdx != -1 { - redactedUri = f.uri[:splitIdx+1] + "redacted-" + strconv.Itoa(f.id) - } - return "{\"Uri\":\"" + redactedUri + "\"}" -} - -func redactedString(s string) string { - if len(s) > 0 { - return "non-empty-string" - } - return "" -} - -func stateJson(state State) string { - keyfiles := []string{} - for _, k := range state.Keyfiles { - keyfiles = append(keyfiles, redactedFile(&k)) - } - keyfilesJson := "[" + strings.Join(keyfiles, ",") + "]" - fields := []string{ - "\"Input\":" + redactedFile(state.Input()), - "\"SaveAs\":" + redactedFile(state.SaveAs), - "\"Comments\":\"" + redactedString(state.Comments.Text) + "\"", - "\"ReedSolomon\":" + strconv.FormatBool(state.ReedSolomon.Checked), - "\"Deniability\":" + strconv.FormatBool(state.Deniability.Checked), - "\"Paranoid\":" + strconv.FormatBool(state.Paranoid.Checked), - "\"Keyfiles\":" + keyfilesJson, - } - return "{" + strings.Join(fields, ",") + "}" -} - -type logLine struct { - time string - action string - state string - err string -} - -type Logger struct { - lines []logLine - mutex sync.Mutex -} - -func (l *Logger) Log(action string, state State, err error) { - errMsg := "" - if err != nil { - errMsg = err.Error() - } - line := logLine{ - time: time.Now().Format("15:04:05.000"), - action: action, - state: stateJson(state), - err: errMsg, - } - l.mutex.Lock() - defer l.mutex.Unlock() - l.lines = append(l.lines, line) -} - -func (l *Logger) CsvString() string { - logLines := []string{"Time,Action,State,Error," + PicoGoVersion} - for _, line := range l.lines { - fields := []string{ - "\"" + line.time + "\"", - "\"" + line.action + "\"", - "\"" + line.state + "\"", - "\"" + line.err + "\"", - } - logLines = append(logLines, strings.Join(fields, ",")) - } - return strings.Join(logLines, "\n") -} diff --git a/internal/ui/state.go b/internal/ui/state.go deleted file mode 100644 index 8a649c2..0000000 --- a/internal/ui/state.go +++ /dev/null @@ -1,274 +0,0 @@ -package ui - -import ( - "fmt" - "strings" - "sync" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/widget" - - "github.com/picocrypt/picogo/internal/encryption" -) - -const PicoGoVersion = "v0.1.5" - -var fileDescCount int -var fileDescMutex sync.Mutex - -func nextFileDescID() int { - fileDescMutex.Lock() - defer fileDescMutex.Unlock() - fileDescCount++ - return fileDescCount -} - -type fileDesc struct { - name string - uri string - id int -} - -func (f *fileDesc) Name() string { - return f.name -} - -func (f *fileDesc) Uri() string { - return f.uri -} - -func NewFileDesc(uri fyne.URI) fileDesc { - return fileDesc{ - name: uri.Name(), - uri: uri.String(), - id: nextFileDescID(), - } -} - -type Settings struct { - ReedSolomonDefault *widget.Check - ParanoidDefault *widget.Check - OrderedKfDefault *widget.Check - DeniabilityDefault *widget.Check -} - -func (s *Settings) Save(app fyne.App) { - preferences := app.Preferences() - preferences.SetBool("ReedSolomonDefault", s.ReedSolomonDefault.Checked) - preferences.SetBool("ParanoidDefault", s.ParanoidDefault.Checked) - preferences.SetBool("OrderedKfDefault", s.OrderedKfDefault.Checked) - preferences.SetBool("DeniabilityDefault", s.DeniabilityDefault.Checked) -} - -func NewSettings(app fyne.App) *Settings { - s := Settings{} - s.ReedSolomonDefault = widget.NewCheck("Reed-Solomon", func(bool) { s.Save(app) }) - s.ParanoidDefault = widget.NewCheck("Paranoid", func(bool) { s.Save(app) }) - s.OrderedKfDefault = widget.NewCheck("Ordered Keyfiles", func(bool) { s.Save(app) }) - s.DeniabilityDefault = widget.NewCheck("Deniability", func(bool) { s.Save(app) }) - - reedSolomon := app.Preferences().Bool("ReedSolomonDefault") - orderedKf := app.Preferences().Bool("OrderedKfDefault") - paranoid := app.Preferences().Bool("ParanoidDefault") - deniability := app.Preferences().Bool("DeniabilityDefault") - - s.ReedSolomonDefault.SetChecked(reedSolomon) - s.ParanoidDefault.SetChecked(paranoid) - s.OrderedKfDefault.SetChecked(orderedKf) - s.DeniabilityDefault.SetChecked(deniability) - return &s -} - -type State struct { - FileName *widget.Label - input *fileDesc - SaveAs *fileDesc - Comments *widget.Entry - ReedSolomon *widget.Check - Deniability *widget.Check - Paranoid *widget.Check - OrderedKeyfiles *widget.Check - Keyfiles []fileDesc - KeyfileText *widget.Entry - Password *widget.Entry - ConfirmPassword *widget.Entry - WorkBtn *widget.Button - Settings *Settings -} - -func NewState(app fyne.App) *State { - state := State{ - FileName: widget.NewLabel(""), - input: nil, - SaveAs: nil, - Comments: makeComments(), - ReedSolomon: widget.NewCheck("Reed-Solomon", nil), - Deniability: widget.NewCheck("Deniability", nil), - Paranoid: widget.NewCheck("Paranoid", nil), - OrderedKeyfiles: widget.NewCheck("Require correct order", nil), - Keyfiles: []fileDesc{}, - KeyfileText: keyfileText(), - Password: makePassword(), - ConfirmPassword: makeConfirmPassword(), - WorkBtn: widget.NewButton("Encrypt", nil), - Settings: NewSettings(app), - } - - state.FileName.Wrapping = fyne.TextWrap(fyne.TextTruncateEllipsis) - state.Deniability.OnChanged = func(checked bool) { state.updateComments() } - - state.ReedSolomon.SetChecked(state.Settings.ReedSolomonDefault.Checked) - state.Paranoid.SetChecked(state.Settings.ParanoidDefault.Checked) - state.OrderedKeyfiles.SetChecked(state.Settings.OrderedKfDefault.Checked) - state.Deniability.SetChecked(state.Settings.DeniabilityDefault.Checked) - - return &state -} - -func (s *State) updateComments() { - if s.Deniability.Checked { - s.Comments.SetText("") - s.Comments.SetPlaceHolder("Comments are disabled in deniability mode") - s.Comments.Disable() - } else { - s.Comments.SetPlaceHolder("Comments are not encrypted") - s.Comments.Enable() - } -} - -func (s *State) Input() *fileDesc { - return s.input -} - -func (s *State) IsEncrypting() bool { - if s.input == nil { - return false - } - return !strings.HasSuffix(s.input.Name(), ".pcv") -} - -func (s *State) IsDecrypting() bool { - if s.input == nil { - return false - } - return strings.HasSuffix(s.input.Name(), ".pcv") -} - -func (s *State) SetInput(input fyne.URI) error { - inputDesc := NewFileDesc(input) - s.input = &inputDesc - - settings := encryption.Settings{ - Comments: "", - ReedSolomon: s.Settings.ReedSolomonDefault.Checked, - Deniability: s.Settings.DeniabilityDefault.Checked, - Paranoid: s.Settings.ParanoidDefault.Checked, - } - if s.IsDecrypting() { - reader, err := storage.Reader(input) - if reader != nil { - defer reader.Close() - } - if err != nil { - return fmt.Errorf("failed to open file: %w", err) - } - settings, err = encryption.GetEncryptionSettings(reader) - if err != nil { - return fmt.Errorf("failed to get encryption settings: %w", err) - } - } - - s.FileName.SetText(s.input.Name()) - - // Set Deniability before Comments to overwrite the result of the callback - s.ReedSolomon.SetChecked(settings.ReedSolomon) - s.Deniability.SetChecked(settings.Deniability) - s.Paranoid.SetChecked(settings.Paranoid) - if s.IsEncrypting() { - s.ReedSolomon.Enable() - s.Deniability.Enable() - s.Paranoid.Enable() - } else { - s.ReedSolomon.Disable() - s.Deniability.Disable() - s.Paranoid.Disable() - } - - s.Comments.SetText(settings.Comments) - if s.IsEncrypting() { - if settings.Deniability { - s.Comments.SetText("") - s.Comments.SetPlaceHolder("Comments are disabled in deniability mode") - s.Comments.Disable() - } else { - s.Comments.SetPlaceHolder("Comments are not encrypted") - s.Comments.Enable() - } - } else { - s.Comments.SetPlaceHolder("") - s.Comments.SetPlaceHolder("") - s.Comments.Disable() - } - - s.OrderedKeyfiles.Enable() - - if s.IsEncrypting() { - s.ConfirmPassword.SetPlaceHolder("Confirm password") - s.ConfirmPassword.Enable() - } else { - s.ConfirmPassword.SetPlaceHolder("Not required") - s.ConfirmPassword.Disable() - } - - if s.IsEncrypting() { - s.WorkBtn.SetText("Encrypt") - s.WorkBtn.Enable() - } else { - s.WorkBtn.SetText("Decrypt") - s.WorkBtn.Enable() - } - - return nil -} - -func (s *State) AddKeyfile(uri fyne.URI) { - s.Keyfiles = append(s.Keyfiles, NewFileDesc(uri)) - names := []string{} - for _, kf := range s.Keyfiles { - names = append(names, kf.Name()) - } - msg := strings.Join(names, "\n") - fyne.Do(func() { s.KeyfileText.SetText(msg) }) -} - -func (s *State) ClearKeyfiles() { - s.Keyfiles = []fileDesc{} - fyne.Do(func() { s.KeyfileText.SetText("No keyfiles added") }) -} - -func (s *State) Clear() { - s.input = nil - s.SaveAs = nil - s.Keyfiles = nil - fyne.DoAndWait(func() { - s.FileName.SetText("") - s.Comments.SetText("") - s.ReedSolomon.SetChecked(s.Settings.ReedSolomonDefault.Checked) - s.Deniability.SetChecked(s.Settings.DeniabilityDefault.Checked) - s.Paranoid.SetChecked(s.Settings.ParanoidDefault.Checked) - s.OrderedKeyfiles.SetChecked(s.Settings.OrderedKfDefault.Checked) - s.Password.SetText("") - s.ConfirmPassword.SetText("") - }) -} - -func (s *State) DefaultSaveName() string { - if s.input == nil { - return "" - } - if s.IsEncrypting() { - return s.input.Name() + ".pcv" - } - return strings.TrimSuffix(s.input.Name(), ".pcv") -} diff --git a/internal/ui/test.pcv b/internal/ui/test.pcv deleted file mode 100644 index 070fb5f..0000000 Binary files a/internal/ui/test.pcv and /dev/null differ diff --git a/internal/ui/theme.go b/internal/ui/theme.go deleted file mode 100644 index 74bf9ef..0000000 --- a/internal/ui/theme.go +++ /dev/null @@ -1,34 +0,0 @@ -package ui - -import ( - "image/color" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/theme" -) - -type Theme struct { - Scale float32 -} - -func (t Theme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { - switch name { - case theme.ColorNameDisabled: - return color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0xff} - default: - return theme.DarkTheme().Color(name, variant) - } -} -func (t Theme) Font(style fyne.TextStyle) fyne.Resource { - return theme.DarkTheme().Font(style) -} -func (t Theme) Icon(name fyne.ThemeIconName) fyne.Resource { - return theme.DarkTheme().Icon(name) -} -func (t Theme) Size(name fyne.ThemeSizeName) float32 { - return theme.DarkTheme().Size(name) * t.Scale -} - -func NewTheme(scale float32) Theme { - return Theme{Scale: scale} -} diff --git a/picogo.go b/picogo.go deleted file mode 100644 index a7c8b87..0000000 --- a/picogo.go +++ /dev/null @@ -1,660 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "fmt" - "image/color" - "io" - "strconv" - "strings" - "time" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/storage" - "fyne.io/fyne/v2/widget" - - "github.com/picocrypt/picogo/internal/encryption" - "github.com/picocrypt/picogo/internal/ui" -) - -const ( - previewTextSize = 5000 -) - -func uriReadCloser(uri string) (fyne.URIReadCloser, error) { - fullUri, err := storage.ParseURI(uri) - if err != nil { - return nil, err - } - return storage.Reader(fullUri) -} - -func uriWriteCloser(uri string) (fyne.URIWriteCloser, error) { - fullUri, err := storage.ParseURI(uri) - if err != nil { - return nil, err - } - return storage.Writer(fullUri) -} - -func clearFile(uri fyne.URI) error { - writer, err := storage.Writer(uri) - if writer != nil { - defer writer.Close() - } - if err != nil { - return err - } - _, err = writer.Write(make([]byte, 1)) - return err -} - -func getOutputURI(app fyne.App) (fyne.URI, error) { - return storage.Child(app.Storage().RootURI(), "output") -} - -func outputReader(app fyne.App) (io.ReadCloser, error) { - outputURI, err := getOutputURI(app) - if err != nil { - return nil, fmt.Errorf("finding output file: %w", err) - } - output, err := uriReadCloser(outputURI.String()) - if err != nil { - return nil, fmt.Errorf("opening output file: %w", err) - } - return output, nil -} - -func clearOutputFile(app fyne.App) error { - outputURI, err := getOutputURI(app) - if err != nil { - return err - } - return clearFile(outputURI) -} - -func chooseSaveAs(logger *ui.Logger, state *ui.State, window fyne.Window, app fyne.App, prepend []byte) { - d := dialog.NewFileSave(func(writer fyne.URIWriteCloser, err error) { - if writer != nil { - defer writer.Close() - } - if err != nil { - logger.Log("Failed while choosing where to save output", *state, err) - dialog.ShowError(fmt.Errorf("choosing output file: %w", err), window) - return - } - if writer != nil { - saveAs := ui.NewFileDesc(writer.URI()) - state.SaveAs = &saveAs - logger.Log("Chose where to save output", *state, nil) - go func() { - saveOutput(logger, state, window, app, prepend) - }() - } else { - logger.Log("Canceled choosing where to save output", *state, nil) - } - }, window) - if input := state.Input(); input == nil { - logger.Log("Failed to seed filename", *state, errors.New("input is nil")) - } else { - if state.IsEncrypting() { - d.SetFileName(input.Name() + ".pcv") - } else { - d.SetFileName(input.Name()[:len(state.Input().Name())-4]) - - } - } - fyne.Do(d.Show) -} - -func saveOutput(logger *ui.Logger, state *ui.State, window fyne.Window, app fyne.App, prepend []byte) { - defer func() { state.SaveAs = nil }() - outputURI, err := getOutputURI(app) - if err != nil { - logger.Log("Get output uri", *state, err) - dialog.ShowError(fmt.Errorf("finding output file: %w", err), window) - return - } - output, err := storage.Reader(outputURI) - if output != nil { - defer output.Close() - } - if err != nil { - logger.Log("Get output reader", *state, err) - dialog.ShowError(fmt.Errorf("opening output file: %w", err), window) - return - } - saveAs, err := uriWriteCloser(state.SaveAs.Uri()) - if saveAs != nil { - defer saveAs.Close() - } - if err != nil { - logger.Log("Get save as writer", *state, err) - dialog.ShowError(fmt.Errorf("opening save-as file: %w", err), window) - return - } - errCh := make(chan error) - counter := ui.ByteCounter{} - go func() { - src := io.MultiReader(bytes.NewBuffer(prepend), output) - _, err := io.Copy(io.MultiWriter(&counter, saveAs), src) - errCh <- err - }() - - progress := widget.NewLabel("") - d := dialog.NewCustomWithoutButtons( - "Saving", - container.New(layout.NewVBoxLayout(), progress), - window, - ) - fyne.Do(d.Show) - - // Block until completion - for { - select { - case err := <-errCh: - fyne.Do(d.Dismiss) - if err != nil { - logger.Log("Saving output", *state, err) - dialog.ShowError(fmt.Errorf("copying output: %w", err), window) - } - err = clearOutputFile(app) - if err != nil { - logger.Log("Clearing output file", *state, err) - dialog.ShowError(fmt.Errorf("cleaning tmp file: %w", err), window) - } - state.Clear() - return - default: - time.Sleep(time.Second / 4) - fyne.Do(func() { - progress.SetText("Total: " + counter.Total() + "\nRate: " + counter.Rate()) - }) - } - } -} - -type encryptResult struct { - header []byte - err error -} - -func encrypt(logger *ui.Logger, state *ui.State, win fyne.Window, app fyne.App) { - counter := ui.ByteCounter{} - resultCh := make(chan encryptResult) - - go func() { - logger.Log("Start encryption routine", *state, nil) - input, err := uriReadCloser(state.Input().Uri()) - if input != nil { - defer input.Close() - } - if err != nil { - logger.Log("Get input reader", *state, err) - resultCh <- encryptResult{nil, fmt.Errorf("getting input file: %w", err)} - return - } - - outputURI, err := getOutputURI(app) - if err != nil { - logger.Log("Get output uri", *state, err) - resultCh <- encryptResult{nil, fmt.Errorf("finding output file: %w", err)} - return - } - output, err := storage.Writer(outputURI) - if output != nil { - defer output.Close() - } - if err != nil { - logger.Log("Get output writer", *state, err) - resultCh <- encryptResult{nil, fmt.Errorf("opening output file: %w", err)} - return - } - - keyfiles := []io.Reader{} - for i := 0; i < len(state.Keyfiles); i++ { - r, err := uriReadCloser(state.Keyfiles[i].Uri()) - if r != nil { - defer r.Close() - } - if err != nil { - logger.Log("Get keyfile reader "+strconv.Itoa(i), *state, err) - resultCh <- encryptResult{nil, fmt.Errorf("getting keyfile %d: %w", i, err)} - return - } - keyfiles = append(keyfiles, r) - } - settings := encryption.Settings{ - Comments: state.Comments.Text, - ReedSolomon: state.ReedSolomon.Checked, - Paranoid: state.Paranoid.Checked, - OrderedKf: state.OrderedKeyfiles.Checked, - Deniability: state.Deniability.Checked, - } - header, err := encryption.EncryptHeadless( - input, state.Password.Text, keyfiles, settings, io.MultiWriter(output, &counter), - ) - if err != nil { - logger.Log("Encrypt headless", *state, err) - resultCh <- encryptResult{nil, fmt.Errorf("encrypting: %w", err)} - return - } - resultCh <- encryptResult{ - header: header, - err: nil, - } - }() - - progress := widget.NewLabel("") - d := dialog.NewCustomWithoutButtons("Encrypting", container.New(layout.NewVBoxLayout(), progress), win) - fyne.Do(d.Show) - var result *encryptResult - for { - select { - case r := <-resultCh: - fyne.Do(d.Dismiss) - result = &r - default: - time.Sleep(time.Second / 4) - fyne.Do(func() { progress.SetText("Total: " + counter.Total() + "\nRate: " + counter.Rate()) }) - } - if result != nil { - break - } - } - logger.Log("Complete encryption", *state, result.err) - if result.err != nil { - dialog.ShowError(fmt.Errorf("encrypting: %w", result.err), win) - return - } - text := widget.NewLabel(state.Input().Name() + " has been encrypted.") - text.Wrapping = fyne.TextWrapWord - fyne.Do(func() { - dialog.ShowCustomConfirm( - "Encryption Complete", - "Save", - "Cancel", - text, - func(b bool) { - if b { - chooseSaveAs(logger, state, win, app, result.header) - } - }, - win, - ) - }) -} - -func tryDecrypt( - logger *ui.Logger, - state *ui.State, - recoveryMode bool, - w fyne.Window, - app fyne.App, -) (bool, error) { - input, err := uriReadCloser(state.Input().Uri()) - if err != nil { - logger.Log("Get input reader", *state, err) - return false, err - } - defer input.Close() - - keyfiles := []io.Reader{} - for i := 0; i < len(state.Keyfiles); i++ { - r, err := uriReadCloser(state.Keyfiles[i].Uri()) - if err != nil { - logger.Log("Get keyfile reader "+strconv.Itoa(i), *state, err) - return false, err - } - defer r.Close() - keyfiles = append(keyfiles, r) - } - - outputURI, err := getOutputURI(app) - if err != nil { - logger.Log("Get output URI", *state, err) - return false, err - } - output, err := uriWriteCloser(outputURI.String()) - if err != nil { - logger.Log("Get output writer", *state, err) - return false, err - } - defer output.Close() - - errCh := make(chan struct { - bool - error - }) - counter := ui.ByteCounter{} - go func() { - logger.Log("Decryption routine start", *state, nil) - damaged, err := encryption.Decrypt( - state.Password.Text, - keyfiles, - input, - io.MultiWriter(output, &counter), - recoveryMode, - ) - errCh <- struct { - bool - error - }{damaged, err} - }() - - // Block until completion - progress := widget.NewLabel("") - d := dialog.NewCustomWithoutButtons("Decrypting", container.New(layout.NewVBoxLayout(), progress), w) - fyne.Do(d.Show) - for { - select { - case err := <-errCh: - fyne.Do(d.Dismiss) - logger.Log("Decryption routine end", *state, err.error) - return err.bool, err.error - default: - time.Sleep(time.Second / 4) - fyne.Do(func() { - progress.SetText("Total: " + counter.Total() + "\nRate: " + counter.Rate()) - }) - } - } -} - -func decrypt(logger *ui.Logger, state *ui.State, win fyne.Window, app fyne.App) { - damaged, err := tryDecrypt(logger, state, false, win, app) - recoveryMode := false - recoveryCanceled := false - if errors.Is(err, encryption.ErrBodyCorrupted) { - // Offer to try again in recovery mode - text := widget.NewLabel("The file is damaged. Would you like to try again in recovery mode?") - text.Wrapping = fyne.TextWrapWord - doneCh := make(chan struct{}) - dialog.ShowCustomConfirm( - "Damaged File", - "Recover", - "Cancel", - text, - func(b bool) { - if b { - logger.Log("Retrying decryption in recovery mode", *state, nil) - recoveryMode = true - damaged, err = tryDecrypt(logger, state, true, win, app) - } else { - recoveryCanceled = true - } - doneCh <- struct{}{} - }, - win, - ) - <-doneCh - } - if recoveryCanceled { - return - } - - logger.Log("Handle decryption result (damaged:"+strconv.FormatBool(damaged)+")", *state, err) - msg := "" - save := false - if err == nil && !damaged { - msg = state.Input().Name() + " has been decrypted." - save = true - } else if err == nil && damaged { - msg = state.Input().Name() + " has been decrypted successfully, but it is damaged. Consider re-encryting and replacing the damaged file." - save = true - } else { - switch { - case errors.Is(err, encryption.ErrBodyCorrupted): - if recoveryMode { - msg = "The file is too damaged to recover. Would you like to save the partially recovered file?" - save = true - } else { - msg = "The file is too damaged to recover." - save = false - } - case errors.Is(err, encryption.ErrIncorrectKeyfiles): - msg = "One or more keyfiles are incorrect." - save = false - case errors.Is(err, encryption.ErrIncorrectPassword): - msg = "The password is incorrect." - save = false - case errors.Is(err, encryption.ErrKeyfilesNotRequired): - msg = "Keyfiles are not required to decrypt this file. Please remove them and try again." - save = false - case errors.Is(err, encryption.ErrKeyfilesRequired): - msg = "Keyfiles are required to decrypt this file. Please add them and try again." - save = false - default: - msg = "Error while decrypting: " + err.Error() - } - } - if save { - text := widget.NewLabel(msg) - text.Wrapping = fyne.TextWrapWord - cancelBtn := widget.NewButton("Cancel", func() {}) - previewBtn := widget.NewButton("Preview", func() { showPreview(app, state, win, logger) }) - saveBtn := widget.NewButton("Save", func() {}) - d := dialog.NewCustomWithoutButtons( - "Decryption Complete", - container.New( - layout.NewVBoxLayout(), - text, - container.New(layout.NewHBoxLayout(), cancelBtn, previewBtn, saveBtn), - ), - win, - ) - cancelBtn.OnTapped = func() { fyne.Do(d.Dismiss) } - saveBtn.OnTapped = func() { - fyne.Do(d.Dismiss) - go func() { chooseSaveAs(logger, state, win, app, []byte{}) }() - } - fyne.Do(d.Show) - } else { - dialog.ShowError(errors.New(msg), win) - } -} - -func wrapWithBorder(c fyne.CanvasObject) *fyne.Container { - border := canvas.NewRectangle(color.White) - border.FillColor = color.Transparent - border.StrokeColor = color.White - border.StrokeWidth = 1 - return container.New( - layout.NewStackLayout(), - border, - container.NewPadded(c), - ) -} - -var developmentWarningShown = false - -func developmentWarning(win fyne.Window) { - if !developmentWarningShown { - dialog.ShowInformation( - "Warning", - "This app is in early development and has not been thoroughly tested", - win, - ) - developmentWarningShown = true - } -} - -func main() { - a := app.New() - theme := ui.NewTheme(1.0) - a.Settings().SetTheme(theme) - w := a.NewWindow("PicoGo") - state := ui.NewState(a) - - logger := ui.Logger{} - logger.Log("Starting PicoGo", *state, nil) - - infoBtn := ui.MakeInfoBtn(w) - logBtn := ui.MakeLogBtn(&logger, w) - info_row := container.New( - layout.NewHBoxLayout(), - infoBtn, - logBtn, - ui.MakeSettingsBtn(state.Settings, w), - layout.NewSpacer(), - ) - - picker := ui.MakeFilePicker(state, &logger, w) - fileRow := wrapWithBorder( - container.New( - layout.NewFormLayout(), - widget.NewLabel("File"), container.NewPadded(container.NewPadded(state.FileName)), - widget.NewLabel("Comments"), container.NewPadded(container.NewPadded(state.Comments)), - ), - ) - - // Advanced encryption settings - advSettingsRow := wrapWithBorder( - container.New( - layout.NewVBoxLayout(), - container.New( - layout.NewVBoxLayout(), - widget.NewRichTextFromMarkdown("### Settings"), - container.New( - layout.NewHBoxLayout(), - state.ReedSolomon, - state.Paranoid, - state.Deniability, - ), - ), - ), - ) - keyfiles := wrapWithBorder( - container.New( - layout.NewVBoxLayout(), - widget.NewRichTextFromMarkdown("### Keyfiles"), - container.NewPadded( - container.NewBorder( - nil, - nil, - container.New( - layout.NewVBoxLayout(), - ui.KeyfileAddBtn(state, &logger, w), - ui.KeyfileCreateBtn(state, &logger, w), - ui.KeyfileClearBtn(state, &logger), - ), - nil, - container.NewPadded( - container.New( - layout.NewVBoxLayout(), - state.OrderedKeyfiles, - state.KeyfileText, - ), - ), - ), - ), - ), - ) - - passwordRow := wrapWithBorder( - container.NewPadded(container.NewPadded( - container.New( - layout.NewVBoxLayout(), - state.Password, - state.ConfirmPassword, - ), - )), - ) - - state.WorkBtn.OnTapped = ui.WorkBtnCallback( - state, - &logger, - w, - func() { go func() { encrypt(&logger, state, w, a) }() }, - func() { go func() { decrypt(&logger, state, w, a) }() }, - ) - - body := container.New( - layout.NewVBoxLayout(), - info_row, - picker, - fileRow, - passwordRow, - advSettingsRow, - keyfiles, - state.WorkBtn, - ) - minSize := fyne.NewSize(body.MinSize().Width*1.05, body.MinSize().Height*1.05) - w.SetContent(container.NewScroll(container.New(layout.NewVBoxLayout(), body))) - - go func() { - for { - // On android, users can change device settings that will scale the size - // of the widgets. This can lead to parts of the app going off screen. - // To work around this, continuously check the size of the window and scale - // the theme to fit. - // - continuous checking is required for desktop because the user may change - // the window size. On android, the app is always full screen, so this should - // only have effect the first loop. - // - only update the theme if the scale is more than 1% different from the current - // scale. This is to prevent constant updates to the theme. - time.Sleep(time.Second / 10.0) - currentSize := w.Canvas().Content().Size() - targetScale := currentSize.Width / minSize.Width - currentScale := theme.Scale - if targetScale > currentScale*1.01 || targetScale < currentScale*0.99 { - theme.Scale = targetScale - fyne.Do(func() { a.Settings().SetTheme(theme) }) - } - } - }() - fyne.Do(func() { developmentWarning(w) }) - w.ShowAndRun() -} - -func showImagePreview(app fyne.App, state *ui.State, window fyne.Window, logger *ui.Logger) { - output, err := outputReader(app) - if err != nil { - dialog.ShowError(fmt.Errorf("opening output file: %w", err), window) - logger.Log("Failed to open output file", *state, err) - return - } - preview := app.NewWindow("Preview") - image := canvas.NewImageFromReader(output, state.Input().Name()) - image.FillMode = canvas.ImageFillContain - preview.SetContent(image) - fyne.Do(preview.Show) -} - -func showTextPreview(app fyne.App, state *ui.State, window fyne.Window, logger *ui.Logger) { - output, err := outputReader(app) - if err != nil { - dialog.ShowError(fmt.Errorf("opening output file: %w", err), window) - logger.Log("Failed to open output file", *state, err) - return - } - // Only read the N bytes to avoid loading too much data into memory - rawText, err := io.ReadAll(io.LimitReader(output, previewTextSize)) - if err != nil { - dialog.ShowError(fmt.Errorf("reading output file: %w", err), window) - logger.Log("Failed to read output file", *state, err) - return - } - text := widget.NewRichTextWithText(string(rawText)) - text.Wrapping = fyne.TextWrapWord - preview := app.NewWindow("Preview") - preview.SetContent(container.NewVScroll(text)) - fyne.Do(preview.Show) -} - -func showPreview(app fyne.App, state *ui.State, window fyne.Window, logger *ui.Logger) { - name := state.DefaultSaveName() - for _, suffix := range []string{".png", ".jpg", ".jpeg", ".svg"} { - if strings.HasSuffix(name, suffix) { - showImagePreview(app, state, window, logger) - return - } - } - showTextPreview(app, state, window, logger) -} diff --git a/picogo_test.go b/picogo_test.go deleted file mode 100644 index 3094f03..0000000 --- a/picogo_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "bytes" - "io" - "os" - "testing" - - "github.com/picocrypt/picogo/internal/ui" -) - -func TestVersion(t *testing.T) { - r, err := os.Open("VERSION") - if err != nil { - t.Fatal(err) - } - defer r.Close() - version, err := io.ReadAll(r) - if err != nil { - t.Fatal(err) - } - if !bytes.Equal(version, []byte(ui.PicoGoVersion)) { - t.Fatal(version, "does not match", []byte(ui.PicoGoVersion), "for version", ui.PicoGoVersion) - } -}