From cdbd664bb9edc4e1b3feee3cb4df639b7c29bd64 Mon Sep 17 00:00:00 2001 From: wyhaya Date: Tue, 7 Jan 2025 14:45:34 +0800 Subject: [PATCH] . --- .github/workflows/ci.yml | 112 ++++++++ .gitignore | 3 + Cargo.lock | 563 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 24 ++ README.md | 68 +++++ src/aef.rs | 233 ++++++++++++++++ src/cli.rs | 151 +++++++++++ src/main.rs | 50 ++++ src/utils.rs | 22 ++ 9 files changed, 1226 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/aef.rs create mode 100644 src/cli.rs create mode 100644 src/main.rs create mode 100644 src/utils.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..52d5cb8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,112 @@ +name: Build + +on: [push, pull_request] + +jobs: + build: + name: ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + strategy: + matrix: + job: + - target: x86_64-unknown-linux-gnu + os: ubuntu-24.04 + cross: false + publish: true + + - target: x86_64-unknown-linux-musl + os: ubuntu-24.04 + cross: true + + - target: aarch64-unknown-linux-gnu + os: ubuntu-24.04 + cross: true + + - target: aarch64-unknown-linux-musl + os: ubuntu-24.04 + cross: true + + - target: x86_64-apple-darwin + os: macos-latest + cross: false + + - target: aarch64-apple-darwin + os: macos-latest + cross: false + + - target: x86_64-pc-windows-msvc + os: windows-latest + cross: false + + env: + NAME: aef + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + run: | + rustup update + rustup target add ${{ matrix.job.target }} + + - name: Install cross + if: matrix.job.cross + run: | + cargo install cross + + - name: Cargo cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ matrix.job.target }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Cargo fmt + run: | + cargo fmt --all -- --check + + - name: Cargo test + if: matrix.job.cross == false + run: | + cargo test + + - name: Cargo Build + if: matrix.job.cross == false + run: | + cargo build --release --target ${{ matrix.job.target }} + + - name: Cross Build + if: matrix.job.cross + run: | + cross build --release --target ${{ matrix.job.target }} + + # -------------- GitHub Relese -------------- + + - name: Package zip (unix) + if: startsWith(github.ref, 'refs/tags/') && matrix.job.os != 'windows-latest' + run: | + cd ./target/${{ matrix.job.target }}/release/ + zip ${{ env.NAME }}-${{ matrix.job.target }}.zip ${{ env.NAME }} + + - name: Package zip (windows) + if: startsWith(github.ref, 'refs/tags/') && matrix.job.os == 'windows-latest' + run: | + cd ./target/${{ matrix.job.target }}/release/ + Compress-Archive -CompressionLevel Optimal -Force -Path ${{ env.NAME }}.exe -DestinationPath ${{ env.NAME }}-${{ matrix.job.target }}.zip + + - name: GitHub release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: ./target/**/*.zip + + # -------------- Cargo publish -------------- + + - name: Cargo publish + if: startsWith(github.ref, 'refs/tags/') && matrix.job.publish + run: | + cargo publish --token ${{ secrets.CARGO_TOKEN }} -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ade610 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +target/ +*.aef \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..12c6c10 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,563 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aef" +version = "0.8.0" +dependencies = [ + "argon2", + "clap", + "dialoguer", + "ring", + "zeroize", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..60a6030 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "aef" +version = "0.8.0" +edition = "2021" +license = "MIT" +description = "An encrypted file archiver" +homepage = "https://github.com/wyhaya/aef" +repository = "https://github.com/wyhaya/aef.git" +readme = "README.md" + +[profile.release] +lto = true +codegen-units = 1 +strip = "symbols" + +[dependencies] +argon2 = "0.5.3" +ring = "0.17.8" +zeroize = "1.8.1" +dialoguer = "0.11.0" +clap = { version = "4.5.23", features = ["derive"] } + +[profile.dev.package.argon2] +opt-level = 3 diff --git a/README.md b/README.md new file mode 100644 index 0000000..80513cf --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ + +# aef [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wyhaya/aef/ci.yml?style=flat-square&branch=main)](https://github.com/wyhaya/aef/actions) [![Crates.io](https://img.shields.io/crates/v/aef.svg?style=flat-square)](https://crates.io/crates/aef) + +`aef` is an encrypted file archiver, it uses `AES-256-GCM` to fully encrypt data and `Argon2id` to prevent brute force data cracking. + +> [!WARNING] +> * aef has not undergone any security check +> * Disruptive changes may occur prior to `1.0` + +## Install + +[Download](https://github.com/wyhaya/aef/releases) the binary from the release page + +Or use `cargo` to install + +```bash +cargo install aef +``` + +## Usage + + +```bash +# Encrypt +aef -i ./your.file -o ./your.file.aef + +# Decrypt +aef -i ./your.file.aef -o ./your.file -d +``` + +#### Password + +By default you will enter your password in the terminal, if you don't want to enter it manually you can use the `-p` option. + +```bash +aef -i ./file -o ./dist.aef -p 123456 +``` + +### Pipeline + +aef support transmission through `Pipeline`, you can use it in combination with commands like `tar`. + +```bash +# Encrypt +tar -czf - your.file | aef -o ./your-file.tgz.aef -p 123456 + +# Decrypt +aef -i ./your-file.tgz.aef -p 123456 | tar -xzf - +``` + +#### Help + +```bash +aef --help +``` + +```bash +Usage: aef [OPTIONS] + +Options: + -i, --input File | Stdin + -o, --output File | Stdout + -p, --password Set password + -d, --decrypt Decrypt file + ... + -h, --help Print help + -V, --version Print version +``` diff --git a/src/aef.rs b/src/aef.rs new file mode 100644 index 0000000..0382924 --- /dev/null +++ b/src/aef.rs @@ -0,0 +1,233 @@ +use crate::cli::Password; +use crate::utils::ThrowError; +use argon2::{Algorithm, Argon2, Params, Version}; +use ring::aead::{ + Aad, BoundKey, Nonce, NonceSequence, OpeningKey, SealingKey, UnboundKey, AES_256_GCM, NONCE_LEN, +}; +use ring::rand::{SecureRandom, SystemRandom}; +use std::fmt::Debug; +use std::io::{Error as IoError, ErrorKind, Read, Result as IoResult, Write}; +use zeroize::Zeroize; + +const AEF_IDENTIFY: &[u8; 4] = b"\xffAEF"; +const AEF_VERSION: u32 = 0x00000001; + +pub const KEY_LEN: usize = 32; +const SALT_LEN: usize = 16; + +const ARGON2_ALGORITHM: Algorithm = Algorithm::Argon2id; +const ARGON2_VERSION: Version = Version::V0x13; +pub const DEFAULT_ARGON2_M: u32 = 256 * 1024; +pub const DEFAULT_ARGON2_T: u32 = 32; +pub const DEFAULT_ARGON2_P: u32 = 4; + +pub enum Error { + Io(IoError), + Encryption, + Decryption, +} + +impl Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(err) => writeln!(f, "IO error {:?}", err), + Self::Encryption => writeln!(f, "Encryption error"), + Self::Decryption => writeln!(f, "Decryption error"), + } + } +} + +#[derive(Debug)] +pub struct FileHeader { + pub salt: [u8; SALT_LEN], + pub params: Params, +} + +impl FileHeader { + pub fn new(salt: [u8; SALT_LEN], params: Params) -> Self { + Self { salt, params } + } + + pub fn write_to(&self, w: &mut W) -> IoResult<()> { + w.write_all(&AEF_IDENTIFY[..])?; + w.write_all(&AEF_VERSION.to_be_bytes())?; + w.write_all(&self.salt)?; + w.write_all(&self.params.m_cost().to_be_bytes())?; + w.write_all(&self.params.t_cost().to_be_bytes())?; + w.write_all(&self.params.p_cost().to_be_bytes())?; + w.flush() + } + + pub fn read_from(r: &mut R) -> IoResult { + let mut identify = [0; 4]; + r.read_exact(&mut identify)?; + if &identify != AEF_IDENTIFY { + return Err(IoError::new(ErrorKind::Other, "Invalid aef identify")); + } + + let mut version = [0; 4]; + r.read_exact(&mut version)?; + if u32::from_be_bytes(version) != AEF_VERSION { + return Err(IoError::new(ErrorKind::Other, "Invalid aef version")); + } + + let mut salt = [0; SALT_LEN]; + r.read_exact(&mut salt)?; + + let mut buf = [0; 4]; + r.read_exact(&mut buf)?; + let m = u32::from_be_bytes(buf); + r.read_exact(&mut buf)?; + let t = u32::from_be_bytes(buf); + r.read_exact(&mut buf)?; + let p = u32::from_be_bytes(buf); + + Ok(Self { + salt, + params: argon2_params(m, t, p), + }) + } +} + +fn rand_bytes(bytes: &mut [u8]) { + SystemRandom::new() + .fill(bytes) + .unwrap_exit(|| "Failed to generate random bytes"); +} + +fn rand_nonce() -> [u8; NONCE_LEN] { + let mut nonce = [0; NONCE_LEN]; + rand_bytes(&mut nonce); + nonce +} + +pub fn rand_salt() -> [u8; SALT_LEN] { + let mut salt = [0; SALT_LEN]; + rand_bytes(&mut salt); + salt +} + +pub fn argon2_params(m: u32, t: u32, p: u32) -> Params { + Params::new(m, t, p, Some(KEY_LEN)).unwrap_exit(|| "Invalid Argon2 parameters") +} + +fn argon2_hash(password: &str, params: Params, salt: [u8; SALT_LEN]) -> [u8; KEY_LEN] { + let mut out = [0; KEY_LEN]; + let argon = Argon2::new(ARGON2_ALGORITHM, ARGON2_VERSION, params); + argon + .hash_password_into(password.as_bytes(), &salt, &mut out) + .unwrap_exit(|| "Failed to argon2id hash password"); + out +} + +struct RandNonce { + nonce: [u8; 12], +} + +impl RandNonce { + fn new(bytes: [u8; NONCE_LEN]) -> Self { + Self { nonce: bytes } + } +} + +impl NonceSequence for RandNonce { + fn advance(&mut self) -> Result { + Ok(Nonce::assume_unique_for_key(self.nonce)) + } +} + +#[derive(Debug)] +pub struct Cipher { + key: [u8; KEY_LEN], +} + +impl Drop for Cipher { + fn drop(&mut self) { + self.key.zeroize(); + } +} + +impl Cipher { + pub fn new(password: &Password, salt: [u8; SALT_LEN]) -> Self { + let key = argon2_hash(&password.password, password.params.clone(), salt); + Self { key } + } + + fn key(&self) -> UnboundKey { + UnboundKey::new(&AES_256_GCM, &self.key).unwrap_exit(|| "Failed to create unbound key") + } + + fn encrypt(&self, nonce: [u8; NONCE_LEN], data: &mut Vec) -> Result<(), Error> { + let nonce = RandNonce::new(nonce); + let mut sealing = SealingKey::new(self.key(), nonce); + sealing + .seal_in_place_append_tag(Aad::empty(), data) + .map_err(|_| Error::Encryption) + } + + fn decrypt<'a>( + &self, + nonce: [u8; NONCE_LEN], + data: &'a mut [u8], + ) -> Result<&'a mut [u8], Error> { + let nonce = RandNonce::new(nonce); + let mut opening = OpeningKey::new(self.key(), nonce); + opening + .open_in_place(Aad::empty(), data) + .map_err(|_| Error::Decryption) + } + + pub fn write_chunk(&self, w: &mut W, mut data: Vec) -> Result<(), Error> { + if data.is_empty() { + w.write_all(&0_u16.to_be_bytes()).map_err(Error::Io)?; + return w.flush().map_err(Error::Io); + } + + let nonce = rand_nonce(); + self.encrypt(nonce, &mut data) + .map_err(|_| Error::Encryption)?; + + let len = data.len() as u16; + w.write_all(&len.to_be_bytes()).map_err(Error::Io)?; + + w.write_all(&nonce).map_err(Error::Io)?; + + w.write_all(&data).map_err(Error::Io)?; + + w.flush().map_err(Error::Io) + } + + pub fn read_chunk(&self, r: &mut R) -> Option, Error>> { + let mut len = [0; 2]; + if let Err(err) = r.read_exact(&mut len) { + if err.kind() == ErrorKind::UnexpectedEof { + return None; + } + return Some(Err(Error::Io(err))); + } + let len = u16::from_be_bytes(len); + if len == 0 { + return None; + } + + let mut nonce = [0; 12]; + if let Err(err) = r.read_exact(&mut nonce) { + return Some(Err(Error::Io(err))); + } + + let mut data = vec![0; len as usize]; + if let Err(err) = r.read_exact(&mut data) { + return Some(Err(Error::Io(err))); + } + + let rst = self + .decrypt(nonce, &mut data) + .map(|data| data.to_vec()) + .map_err(|_| Error::Decryption); + + Some(rst) + } +} + +#[cfg(test)] +mod tests {} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..2c89ebb --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,151 @@ +use crate::aef::{argon2_params, DEFAULT_ARGON2_M, DEFAULT_ARGON2_P, DEFAULT_ARGON2_T}; +use crate::utils::ThrowError; +use argon2::Params; +use clap::Parser; +use std::fs::File; +use std::io::{stdin, stdout, Read, Result, Stdin, Stdout, Write}; +use zeroize::Zeroize; + +#[derive(Parser, Debug)] +#[clap(version, about)] +struct Args { + /// File | Stdin + #[clap(short, long)] + input: Option, + + /// File | Stdout + #[clap(short, long)] + output: Option, + + /// Set password + #[clap(short, long)] + password: Option, + + /// Decrypt file + #[clap(short, long)] + decrypt: bool, + + /// Argon2: memory size in 1 KiB blocks. Between 8 * `argon2_p` and (2^32)-1 + #[clap(long, name = "M", default_value_t = DEFAULT_ARGON2_M)] + argon2_m: u32, + + /// Argon2: number of iterations. Between 1 and (2^32)-1 + #[clap(long, name = "T", default_value_t = DEFAULT_ARGON2_T)] + argon2_t: u32, + + /// Argon2: degree of parallelism. Between 1 and (2^24)-1 + #[clap(long, name = "P", default_value_t = DEFAULT_ARGON2_P)] + argon2_p: u32, +} + +#[derive(Debug)] +pub enum Input { + Stdin(Stdin), + File(File), +} + +#[derive(Debug)] +pub enum Output { + Stdout(Stdout), + File(LazyFile), +} + +impl Read for Input { + #[inline] + fn read(&mut self, buf: &mut [u8]) -> Result { + match self { + Self::Stdin(io) => io.read(buf), + Self::File(io) => io.read(buf), + } + } +} + +impl Write for Output { + #[inline] + fn write(&mut self, buf: &[u8]) -> Result { + match self { + Self::Stdout(io) => io.write(buf), + Self::File(io) => io.get_mut().write(buf), + } + } + + #[inline] + fn flush(&mut self) -> Result<()> { + match self { + Self::Stdout(io) => io.flush(), + Self::File(io) => io.get_mut().flush(), + } + } +} + +impl Input { + fn from_path(path: String) -> Self { + File::open(&path) + .map(Input::File) + .unwrap_exit(|| format!("Failed to open file '{}'", path)) + } +} + +impl Output { + fn from_path(path: String) -> Self { + Output::File(LazyFile::Path(path)) + } +} + +#[derive(Debug)] +pub enum LazyFile { + Path(String), + File(File), +} + +impl LazyFile { + fn get_mut(&mut self) -> &mut File { + match self { + Self::Path(path) => { + let file = File::create_new(&path) + .unwrap_exit(|| format!("Failed to create file '{}'", path)); + *self = Self::File(file); + self.get_mut() + } + Self::File(file) => file, + } + } +} + +pub struct Password { + pub password: String, + pub params: Params, +} + +impl Drop for Password { + fn drop(&mut self) { + self.password.zeroize(); + } +} + +pub fn parse() -> (Input, Output, Password, bool) { + let args = Args::parse(); + + let params = argon2_params(args.argon2_m, args.argon2_t, args.argon2_p); + let password = args.password.unwrap_or_else(|| { + let mut read = dialoguer::Password::new().with_prompt("Password"); + if !args.decrypt { + read = read + .with_confirmation("Confirm password", "Passwords mismatching, please re-enter"); + } + read.interact().unwrap_exit(|| "Read password") + }); + let password = Password { password, params }; + + let input = args + .input + .map(Input::from_path) + .unwrap_or_else(|| Input::Stdin(stdin())); + + let output = args + .output + .map(Output::from_path) + .unwrap_or_else(|| Output::Stdout(stdout())); + + (input, output, password, args.decrypt) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..62a4fac --- /dev/null +++ b/src/main.rs @@ -0,0 +1,50 @@ +mod aef; +mod cli; +mod utils; + +use aef::{rand_salt, Cipher, Error, FileHeader}; +use cli::{Input, Output, Password}; +use std::io::{Read, Write}; + +fn main() { + let (input, output, password, de) = cli::parse(); + if de { + decrypt(input, output, password) + } else { + encrypt(input, output, password) + } + .unwrap_or_else(|err| exit!("{:?}", err)); +} + +fn decrypt(mut input: Input, mut output: Output, mut password: Password) -> Result<(), Error> { + let FileHeader { salt, params } = FileHeader::read_from(&mut input).map_err(Error::Io)?; + password.params = params; + let cipher = Cipher::new(&password, salt); + loop { + let rst = cipher.read_chunk(&mut input); + match rst { + Some(Ok(data)) => { + output.write_all(&data).map_err(Error::Io)?; + } + Some(Err(err)) => return Err(err), + None => break, + } + } + Ok(()) +} + +fn encrypt(mut input: Input, mut output: Output, password: Password) -> Result<(), Error> { + let salt = rand_salt(); + let cipher = Cipher::new(&password, salt); + FileHeader::new(salt, password.params.clone()) + .write_to(&mut output) + .map_err(Error::Io)?; + let mut buf = [0; 32 * 1024]; + loop { + let n = input.read(&mut buf).map_err(Error::Io)?; + if n == 0 { + return cipher.write_chunk(&mut output, Vec::new()); + } + cipher.write_chunk(&mut output, buf[..n].to_vec())?; + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..4c0116b --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,22 @@ +use std::fmt::{Debug, Display}; + +#[macro_export] +macro_rules! exit { + ($($arg:tt)*) => { + { + eprint!("Error: "); + eprintln!($($arg)*); + std::process::exit(1) + } + }; +} + +pub trait ThrowError D, T> { + fn unwrap_exit(self, f: F) -> T; +} + +impl D, T> ThrowError for Result { + fn unwrap_exit(self, f: F) -> T { + self.unwrap_or_else(|err| exit!("{} {:?}", f(), err)) + } +}