From 5abecef170bc8e988bb83af400633fff23a176fb Mon Sep 17 00:00:00 2001 From: leo <77177015+greendoescode@users.noreply.github.com> Date: Mon, 24 Jun 2024 23:38:33 +0100 Subject: [PATCH] feature!: add volaris-domain and volaris-cli code (#2) * docs: update readme to include more information * feat: add volariscore code - copied from dexios * fix: fixing cargolocks * chore: update cargo.toml * feature!: added volaris-domain and volaris-cli code (thanks jake) * fix: remove cargo.lock * fix: add cargo.lock? --- Cargo.lock | 573 -------------- Cargo.toml | 3 +- crates/volaris-cli/Cargo.toml | 30 +- crates/volaris-cli/README.md | 22 + crates/volaris-cli/src/cli.rs | 567 +++++++++++++ crates/volaris-cli/src/cli/prompt.rs | 82 ++ crates/volaris-cli/src/global.rs | 39 + crates/volaris-cli/src/global/parameters.rs | 238 ++++++ crates/volaris-cli/src/global/states.rs | 184 +++++ crates/volaris-cli/src/global/structs.rs | 29 + crates/volaris-cli/src/main.rs | 70 +- crates/volaris-cli/src/subcommands.rs | 157 ++++ crates/volaris-cli/src/subcommands/decrypt.rs | 64 ++ crates/volaris-cli/src/subcommands/encrypt.rs | 82 ++ crates/volaris-cli/src/subcommands/erase.rs | 47 ++ crates/volaris-cli/src/subcommands/hashing.rs | 26 + crates/volaris-cli/src/subcommands/header.rs | 139 ++++ crates/volaris-cli/src/subcommands/key.rs | 171 ++++ crates/volaris-cli/src/subcommands/pack.rs | 129 +++ crates/volaris-cli/src/subcommands/unpack.rs | 81 ++ crates/volaris-core/README.md | 9 +- crates/volaris-domain/Cargo.toml | 23 + crates/volaris-domain/README.md | 1 + crates/volaris-domain/src/decrypt.rs | 263 +++++++ crates/volaris-domain/src/encrypt.rs | 337 ++++++++ crates/volaris-domain/src/erase.rs | 93 +++ crates/volaris-domain/src/erase_dir.rs | 109 +++ crates/volaris-domain/src/hash.rs | 126 +++ crates/volaris-domain/src/hasher.rs | 26 + crates/volaris-domain/src/header.rs | 31 + crates/volaris-domain/src/header/dump.rs | 31 + crates/volaris-domain/src/header/restore.rs | 52 ++ crates/volaris-domain/src/header/strip.rs | 42 + crates/volaris-domain/src/key.rs | 109 +++ crates/volaris-domain/src/key/add.rs | 99 +++ crates/volaris-domain/src/key/change.rs | 93 +++ crates/volaris-domain/src/key/delete.rs | 65 ++ crates/volaris-domain/src/key/verify.rs | 43 + crates/volaris-domain/src/lib.rs | 67 ++ crates/volaris-domain/src/overwrite.rs | 125 +++ crates/volaris-domain/src/pack.rs | 244 ++++++ crates/volaris-domain/src/storage.rs | 743 ++++++++++++++++++ crates/volaris-domain/src/unpack.rs | 177 +++++ crates/volaris-domain/src/utils.rs | 56 ++ crates/volaris-domain/tests/common.rs | 89 +++ crates/volaris-domain/tests/storage.rs | 277 +++++++ 46 files changed, 5477 insertions(+), 586 deletions(-) create mode 100644 crates/volaris-cli/README.md create mode 100644 crates/volaris-cli/src/cli.rs create mode 100644 crates/volaris-cli/src/cli/prompt.rs create mode 100644 crates/volaris-cli/src/global.rs create mode 100644 crates/volaris-cli/src/global/parameters.rs create mode 100644 crates/volaris-cli/src/global/states.rs create mode 100644 crates/volaris-cli/src/global/structs.rs create mode 100644 crates/volaris-cli/src/subcommands.rs create mode 100644 crates/volaris-cli/src/subcommands/decrypt.rs create mode 100644 crates/volaris-cli/src/subcommands/encrypt.rs create mode 100644 crates/volaris-cli/src/subcommands/erase.rs create mode 100644 crates/volaris-cli/src/subcommands/hashing.rs create mode 100644 crates/volaris-cli/src/subcommands/header.rs create mode 100644 crates/volaris-cli/src/subcommands/key.rs create mode 100644 crates/volaris-cli/src/subcommands/pack.rs create mode 100644 crates/volaris-cli/src/subcommands/unpack.rs create mode 100644 crates/volaris-domain/Cargo.toml create mode 100644 crates/volaris-domain/README.md create mode 100644 crates/volaris-domain/src/decrypt.rs create mode 100644 crates/volaris-domain/src/encrypt.rs create mode 100644 crates/volaris-domain/src/erase.rs create mode 100644 crates/volaris-domain/src/erase_dir.rs create mode 100644 crates/volaris-domain/src/hash.rs create mode 100644 crates/volaris-domain/src/hasher.rs create mode 100644 crates/volaris-domain/src/header.rs create mode 100644 crates/volaris-domain/src/header/dump.rs create mode 100644 crates/volaris-domain/src/header/restore.rs create mode 100644 crates/volaris-domain/src/header/strip.rs create mode 100644 crates/volaris-domain/src/key.rs create mode 100644 crates/volaris-domain/src/key/add.rs create mode 100644 crates/volaris-domain/src/key/change.rs create mode 100644 crates/volaris-domain/src/key/delete.rs create mode 100644 crates/volaris-domain/src/key/verify.rs create mode 100644 crates/volaris-domain/src/lib.rs create mode 100644 crates/volaris-domain/src/overwrite.rs create mode 100644 crates/volaris-domain/src/pack.rs create mode 100644 crates/volaris-domain/src/storage.rs create mode 100644 crates/volaris-domain/src/unpack.rs create mode 100644 crates/volaris-domain/src/utils.rs create mode 100644 crates/volaris-domain/tests/common.rs create mode 100644 crates/volaris-domain/tests/storage.rs diff --git a/Cargo.lock b/Cargo.lock index b96c657..e69de29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,573 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" - -[[package]] -name = "argon2" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" -dependencies = [ - "base64ct", - "blake2", - "password-hash", -] - -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "balloon-hash" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdbafd8c776e0a7c250b5dfaf4723477e0d92b42c5a91ce4e50bd9bb2dcc9a1" -dependencies = [ - "crypto-bigint", - "digest", - "password-hash", -] - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "blake3" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "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.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "windows-sys", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" - -[[package]] -name = "cpufeatures" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "deoxys" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00603a49e114bd99d87a4ab8d480f36ecd1451a9d6474f66973d1a829ff77789" -dependencies = [ - "aead", - "aes", - "subtle", - "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 = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[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 = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "indicatif" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" -dependencies = [ - "console", - "lazy_static", - "number_prefix", - "regex", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.155" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "regex" -version = "1.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "volaris" -version = "0.0.0" - -[[package]] -name = "volaris-core" -version = "0.0.1" -dependencies = [ - "aead", - "aes-gcm", - "anyhow", - "argon2", - "balloon-hash", - "blake3", - "chacha20poly1305", - "deoxys", - "indicatif", - "rand", - "zeroize", -] - -[[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-targets" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" -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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" - -[[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 index e9182c8..0b4579b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = ["crates/*"] +resolver = "2" [workspace.package] -authors = ["Jake "] +authors = ["Jake ", "Leo "] license = "BSD-2-Clause" diff --git a/crates/volaris-cli/Cargo.toml b/crates/volaris-cli/Cargo.toml index 63eaf7b..fbe2368 100644 --- a/crates/volaris-cli/Cargo.toml +++ b/crates/volaris-cli/Cargo.toml @@ -1,9 +1,29 @@ [package] -name = "volaris" -version = "0.0.0" +name = "volaris-cli" +version = "0.0.1" +authors = ["brxken128 ", "greendoescode "] +readme = "README.md" edition = "2021" -authors.workspace = true -license.workspace = true -description = "A Rust file encryption tool" +description = "Secure, fast and authenticated command-line encryption of files with modern algorithms and an audited encryption backend." +keywords = ["encryption", "utility", "file", "command-line", "secure"] +categories = ["cryptography", "command-line-utilities"] +repository = "https://github.com/volar-is/volaris/tree/master/volaris" +homepage = "https://github.com/volar-is/volaris" +license = "BSD-2-Clause" + +# this is for sites other than crates.io, who may still use it +[badges] +maintenance = { status = "actively-developed" } [dependencies] +blake3 = "1.3.3" +rand = "0.8.5" + +domain = { package = "volaris-domain", version = "0.0.1", path = "../volaris-domain" } +core = { package = "volaris-core", path = "../volaris-core", version = "0.0.1" } + +clap = { version = "3.2.21", features = ["cargo"] } +anyhow = "1.0.65" + +zip = { version = "0.6.3", default-features = false, features = ["zstd"] } +rpassword = "7.2" diff --git a/crates/volaris-cli/README.md b/crates/volaris-cli/README.md new file mode 100644 index 0000000..9d3dfcd --- /dev/null +++ b/crates/volaris-cli/README.md @@ -0,0 +1,22 @@ +# Volaris +![GitHub License](https://img.shields.io/github/license/volarisapp/Volaris) ![GitHub Issues](https://img.shields.io/github/issues/volarisapp/Volaris) ![GitHub Stars](https://img.shields.io/github/stars/volarisapp/Volaris) + +## Introduction + +Volaris is an encryption tool designed to prioritize privacy and security. Built using Rust, it offers a modern and efficient solution for securing your data across multiple platforms, including desktops, command-line interfaces (CLI), and mobile devices. + +This specific module is the 'CLI' version. + +## Features + +- **Cross-Platform Support**: Available on desktop, CLI, and mobile devices. +- **Rust-Based Security**: Leveraging Rust’s safety and performance features. +- **Modern Encryption Standards**: Uses the latest encryption algorithms to ensure data security. + +## Current Status + +Volaris is currently in development. We are working hard to bring you a secure and user-friendly encryption tool. Stay tuned for updates and releases. + +## Contact + +For any questions or support, please open an issue on GitHub. diff --git a/crates/volaris-cli/src/cli.rs b/crates/volaris-cli/src/cli.rs new file mode 100644 index 0000000..a3d2a7e --- /dev/null +++ b/crates/volaris-cli/src/cli.rs @@ -0,0 +1,567 @@ +use clap::{Arg, Command}; + +pub mod prompt; + +// this defines all of the clap subcommands and arguments +// it's long, and clunky, but i feel that's just the nature of the clap builder api +// it returns the ArgMatches so that a match statement can send everything to the correct place +#[allow(clippy::too_many_lines)] +pub fn get_matches() -> clap::ArgMatches { + let encrypt = Command::new("encrypt") + .short_flag('e') + .about("Encrypt a file") + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The file to encrypt"), + ) + .arg( + Arg::new("output") + .value_name("output") + .takes_value(true) + .required(true) + .help("The output file"), + ) + .arg( + Arg::new("keyfile") + .short('k') + .long("keyfile") + .value_name("file") + .takes_value(true) + .help("Use a keyfile instead of a password"), + ) + .arg( + Arg::new("erase") + .long("erase") + .value_name("# of passes") + .takes_value(true) + .require_equals(true) + .help("Securely erase the input file once complete (default is 1 pass)") + .min_values(0) + .default_missing_value("1"), + ) + .arg( + Arg::new("hash") + .short('H') + .long("hash") + .takes_value(false) + .help("Return a BLAKE3 hash of the encrypted file"), + ) + .arg( + Arg::new("argon") + .long("argon") + .takes_value(false) + .help("Use argon2id for password hashing"), + ) + .arg( + Arg::new("autogenerate") + .long("auto") + .value_name("# of words") + .min_values(0) + .default_missing_value("7") + .takes_value(true) + .require_equals(true) + .help("Autogenerate a passphrase (default is 7 words)") + .conflicts_with("keyfile"), + ) + .arg( + Arg::new("header") + .long("header") + .value_name("file") + .takes_value(true) + .help("Store the header separately from the file"), + ) + .arg( + Arg::new("force") + .short('f') + .long("force") + .takes_value(false) + .help("Force all actions"), + ) + .arg( + Arg::new("aes") + .long("aes") + .takes_value(false) + .help("Use AES-256-GCM for encryption"), + ); + + let decrypt = Command::new("decrypt") + .short_flag('d') + .about("Decrypt a file") + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The file to decrypt"), + ) + .arg( + Arg::new("output") + .value_name("output") + .takes_value(true) + .required(true) + .help("The output file"), + ) + .arg( + Arg::new("keyfile") + .short('k') + .long("keyfile") + .value_name("file") + .takes_value(true) + .help("Use a keyfile instead of a password"), + ) + .arg( + Arg::new("header") + .long("header") + .value_name("file") + .takes_value(true) + .help("Use a header file that was dumped"), + ) + .arg( + Arg::new("erase") + .long("erase") + .value_name("# of passes") + .takes_value(true) + .require_equals(true) + .help("Securely erase the input file once complete (default is 1 pass)") + .min_values(0) + .default_missing_value("1"), + ) + .arg( + Arg::new("hash") + .short('H') + .long("hash") + .takes_value(false) + .help("Return a BLAKE3 hash of the encrypted file"), + ) + .arg( + Arg::new("force") + .short('f') + .long("force") + .takes_value(false) + .help("Force all actions"), + ); + + Command::new("volaris") + .version(clap::crate_version!()) + .author("brxken128 ") + .about("Secure, fast and modern command-line encryption of files.") + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand(encrypt.clone()) + .subcommand(decrypt.clone()) + .subcommand( + Command::new("erase") + .about("Erase a file completely") + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The file to erase"), + ) + .arg( + Arg::new("force") + .short('f') + .long("force") + .takes_value(false) + .help("Force all actions"), + ) + .arg( + Arg::new("passes") + .long("passes") + .value_name("# of passes") + .takes_value(true) + .require_equals(true) + .help("Specify the number of passes (default is 1)") + .min_values(0) + .default_missing_value("1"), + ), + ) + .subcommand( + Command::new("hash").about("Hash files with BLAKE3").arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The file(s) to hash") + .min_values(1) + .multiple_occurrences(true), + ), + ) + .subcommand( + Command::new("pack") + .about("Pack and encrypt an entire directory") + .short_flag('p') + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .multiple_values(true) + .required(true) + .help("The directory to encrypt"), + ) + .arg( + Arg::new("output") + .value_name("output") + .takes_value(true) + .required(true) + .help("The output file"), + ) + .arg( + Arg::new("erase") + .long("erase") + .takes_value(false) + .help("Securely erase every file from the source directory, before deleting the directory") + ) + .arg( + Arg::new("argon") + .long("argon") + .takes_value(false) + .help("Use argon2id for password hashing"), + ) + .arg( + Arg::new("verbose") + .short('v') + .long("verbose") + .takes_value(false) + .help("Show a detailed output"), + ) + .arg( + Arg::new("autogenerate") + .long("auto") + .value_name("# of words") + .min_values(0) + .default_missing_value("7") + .takes_value(true) + .require_equals(true) + .help("Autogenerate a passphrase (default is 7 words)") + .conflicts_with("keyfile"), + ) + .arg( + Arg::new("header") + .long("header") + .value_name("file") + .takes_value(true) + .help("Store the header separately from the file"), + ) + .arg( + Arg::new("zstd") + .short('z') + .long("zstd") + .takes_value(false) + .help("Use ZSTD compression"), + ) + .arg( + Arg::new("recursive") + .short('r') + .long("recursive") + .takes_value(false) + .help("Index files and folders within other folders (index recursively)"), + ) + .arg( + Arg::new("keyfile") + .short('k') + .long("keyfile") + .value_name("file") + .takes_value(true) + .help("Use a keyfile instead of a password"), + ) + .arg( + Arg::new("hash") + .short('H') + .long("hash") + .takes_value(false) + .help("Return a BLAKE3 hash of the encrypted file"), + ) + .arg( + Arg::new("force") + .short('f') + .long("force") + .takes_value(false) + .help("Force all actions"), + ) + .arg( + Arg::new("aes") + .long("aes") + .takes_value(false) + .help("Use AES-256-GCM for encryption"), + ) + ) + .subcommand( + Command::new("unpack") + .short_flag('u') + .about("Unpack a previously-packed file") + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The file to decrypt"), + ) + .arg( + Arg::new("output") + .value_name("output") + .takes_value(true) + .required(true) + .help("The output file"), + ) + .arg( + Arg::new("keyfile") + .short('k') + .long("keyfile") + .value_name("file") + .takes_value(true) + .help("Use a keyfile instead of a password"), + ) + .arg( + Arg::new("header") + .long("header") + .value_name("file") + .takes_value(true) + .help("Use a header file that was dumped"), + ) + .arg( + Arg::new("erase") + .long("erase") + .value_name("# of passes") + .takes_value(true) + .require_equals(true) + .help("Securely erase the input file once complete (default is 1 pass)") + .min_values(0) + .default_missing_value("1"), + ) + .arg( + Arg::new("verbose") + .short('v') + .long("verbose") + .takes_value(false) + .help("Show a detailed output"), + ) + .arg( + Arg::new("hash") + .short('H') + .long("hash") + .takes_value(false) + .help("Return a BLAKE3 hash of the encrypted file"), + ) + .arg( + Arg::new("force") + .short('f') + .long("force") + .takes_value(false) + .help("Force all actions"), + ) + ) + .subcommand(Command::new("key") + .about("Manipulate keys within the header (for advanced users") + .subcommand_required(true) + .subcommand( + Command::new("change") + .about("Change an encrypted file's key") + .arg_required_else_help(true) + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The encrypted file/header file"), + ) + .arg( + Arg::new("autogenerate") + .long("auto") + .value_name("# of words") + .min_values(0) + .default_missing_value("7") + .takes_value(true) + .require_equals(true) + .help("Autogenerate a passphrase (default is 7 words)") + .conflicts_with("keyfile"), + ) + .arg( + Arg::new("argon") + .long("argon") + .takes_value(false) + .help("Use argon2id for password hashing"), + ) + .arg( + Arg::new("keyfile-old") + .short('k') + .long("keyfile-old") + .value_name("file") + .takes_value(true) + .help("Use an old keyfile to decrypt the master key"), + ) + .arg( + Arg::new("keyfile-new") + .short('n') + .long("keyfile-new") + .value_name("file") + .takes_value(true) + .help("Use a keyfile as the new key"), + ), + ) + .subcommand( + Command::new("add") + .about("Add a key to an encrypted file (for advanced users)") + .arg_required_else_help(true) + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The encrypted file/header file"), + ) + .arg( + Arg::new("argon") + .long("argon") + .takes_value(false) + .help("Use argon2id for password hashing"), + ) + .arg( + Arg::new("autogenerate") + .long("auto") + .value_name("# of words") + .min_values(0) + .default_missing_value("7") + .takes_value(true) + .require_equals(true) + .help("Autogenerate a passphrase (default is 7 words)") + .conflicts_with("keyfile"), + ) + .arg( + Arg::new("keyfile-old") + .short('k') + .long("keyfile-old") + .value_name("file") + .takes_value(true) + .help("Use an old keyfile to decrypt the master key"), + ) + .arg( + Arg::new("keyfile-new") + .short('n') + .long("keyfile-new") + .value_name("file") + .takes_value(true) + .help("Use a keyfile as the new key"), + ), + ) + .subcommand( + Command::new("del") + .about("Delete a key from an encrypted file (for advanced users)") + .arg_required_else_help(true) + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The encrypted file/header file"), + ) + .arg( + Arg::new("keyfile") + .short('k') + .long("keyfile") + .value_name("file") + .takes_value(true) + .help("Use a keyfile to identify the key you want to delete"), + ), + ) + .subcommand( + Command::new("verify") + .about("Verify that a key is correct") + .arg_required_else_help(true) + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The encrypted file/header file"), + ) + .arg( + Arg::new("keyfile") + .short('k') + .long("keyfile") + .value_name("file") + .takes_value(true) + .help("Verify a keyfile"), + ), + ) + ) + .subcommand( + Command::new("header") + .about("Manipulate encrypted headers (for advanced users)") + .subcommand_required(true) + .subcommand( + Command::new("dump") + .about("Dump a header") + .arg_required_else_help(true) + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The encrypted file"), + ) + .arg( + Arg::new("output") + .value_name("output") + .takes_value(true) + .required(true) + .help("The output file"), + ) + .arg( + Arg::new("force") + .short('f') + .long("force") + .takes_value(false) + .help("Force all actions"), + ), + ) + .subcommand( + Command::new("restore") + .about("Restore a header") + .arg_required_else_help(true) + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The dumped header file"), + ) + .arg( + Arg::new("output") + .value_name("output") + .takes_value(true) + .required(true) + .help("The encrypted file"), + ), + ) + .subcommand( + Command::new("strip") + .about("Strip a header") + .arg_required_else_help(true) + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The encrypted file"), + ), + ) + .subcommand( + Command::new("details") + .about("Show details of a header") + .arg_required_else_help(true) + .arg( + Arg::new("input") + .value_name("input") + .takes_value(true) + .required(true) + .help("The encrypted/header file"), + ), + ), + ) + .get_matches() +} diff --git a/crates/volaris-cli/src/cli/prompt.rs b/crates/volaris-cli/src/cli/prompt.rs new file mode 100644 index 0000000..1dfbe63 --- /dev/null +++ b/crates/volaris-cli/src/cli/prompt.rs @@ -0,0 +1,82 @@ +use anyhow::{Context, Result}; +use std::io::{self, stdin, Write}; + +use crate::{ + global::states::{ForceMode, PasswordState}, + question, warn, +}; + +use core::protected::Protected; +use core::Zeroize; + +// this handles user-interactivity, specifically getting a "yes" or "no" answer from the user +// it requires the question itself, if the default is true/false +// if force is enabled then it will just return the `default` +pub fn get_answer(prompt: &str, default: bool, force: ForceMode) -> Result { + if force == ForceMode::Force { + return Ok(true); + } + + let switch = if default { "(Y/n)" } else { "(y/N)" }; + + let answer_bool = loop { + question!("{prompt} {switch}: "); + io::stdout().flush().context("Unable to flush stdout")?; + + let mut answer = String::new(); + stdin() + .read_line(&mut answer) + .context("Unable to read from stdin")?; + + let answer_lowercase = answer.to_lowercase(); + let first_char = answer_lowercase + .chars() + .next() + .context("Unable to get first character of your answer")?; + break match first_char { + '\n' | '\r' => default, + 'y' => true, + 'n' => false, + _ => { + warn!("Unrecognised answer - please try again"); + continue; + } + }; + }; + Ok(answer_bool) +} + +// this checks if the file exists +// then it prompts the user if they'd like to overwrite a file (while showing the associated file name) +// if they have the force argument supplied, this will just assume true +// if force mode is true, avoid prompts at all +pub fn overwrite_check(name: &str, force: ForceMode) -> Result { + let answer = if std::fs::metadata(name).is_ok() { + let prompt = format!("{} already exists, would you like to overwrite?", name); + get_answer(&prompt, true, force)? + } else { + true + }; + Ok(answer) +} + +pub fn get_password(pass_state: &PasswordState) -> Result>> { + Ok(loop { + let input = rpassword::prompt_password("Password: ").context("Unable to read password")?; + if pass_state == &PasswordState::Direct { + return Ok(Protected::new(input.into_bytes())); + } + + let mut input_validation = + rpassword::prompt_password("Confirm password: ").context("Unable to read password")?; + + if input == input_validation && !input.is_empty() { + input_validation.zeroize(); + break Protected::new(input.into_bytes()); + } else if input.is_empty() { + warn!("Password cannot be empty, please try again."); + } else { + warn!("The passwords aren't the same, please try again."); + } + }) +} diff --git a/crates/volaris-cli/src/global.rs b/crates/volaris-cli/src/global.rs new file mode 100644 index 0000000..3198e9c --- /dev/null +++ b/crates/volaris-cli/src/global.rs @@ -0,0 +1,39 @@ +pub mod parameters; +pub mod states; +pub mod structs; + +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => { + println!("[i] {}", format!($($arg)*)) + } +} + +#[macro_export] +macro_rules! error { + ($($arg:tt)*) => { + println!("[!] {}", format!($($arg)*)) + } +} + +#[macro_export] +macro_rules! success { + ($($arg:tt)*) => { + println!("[+] {}", format!($($arg)*)) + } +} + +#[macro_export] +macro_rules! warn { + ($($arg:tt)*) => { + println!("[-] {}", format!($($arg)*)) + } +} + +#[macro_export] +macro_rules! question { + ($($arg:tt)*) => { + print!("[?] {}", format!($($arg)*)); + + } +} diff --git a/crates/volaris-cli/src/global/parameters.rs b/crates/volaris-cli/src/global/parameters.rs new file mode 100644 index 0000000..5031df5 --- /dev/null +++ b/crates/volaris-cli/src/global/parameters.rs @@ -0,0 +1,238 @@ +// this file handles getting parameters from clap's ArgMatches +// it returns information (e.g. CryptoParams) to functions that require it + +use crate::global::states::{EraseMode, EraseSourceDir, ForceMode, HashMode, HeaderLocation}; +use crate::global::structs::CryptoParams; +use crate::global::structs::PackParams; +use crate::warn; +use anyhow::{Context, Result}; +use clap::ArgMatches; +use core::header::{HashingAlgorithm, ARGON2ID_LATEST, BLAKE3BALLOON_LATEST}; +use core::primitives::Algorithm; + +use super::states::{Compression, DirectoryMode, Key, KeyParams, PrintMode}; +use super::structs::KeyManipulationParams; + +pub fn get_params(name: &str, sub_matches: &ArgMatches) -> Result> { + let values = sub_matches + .get_many::(name) + .with_context(|| format!("No {name} provided"))? + .map(String::from) + .collect(); + Ok(values) +} + +pub fn get_param(name: &str, sub_matches: &ArgMatches) -> Result { + let value = sub_matches + .value_of(name) + .with_context(|| format!("No {} provided", name))? + .to_string(); + Ok(value) +} + +// the main parameter handler for encrypt/decrypt +pub fn parameter_handler(sub_matches: &ArgMatches) -> Result { + let key = Key::init(sub_matches, &KeyParams::default(), "keyfile")?; + + let hash_mode = if sub_matches.is_present("hash") { + //specify to emit hash after operation + HashMode::CalculateHash + } else { + // default + HashMode::NoHash + }; + + let force = forcemode(sub_matches); + + let erase = if sub_matches.is_present("erase") { + let result = sub_matches + .value_of("erase") + .context("No amount of passes specified")? + .parse(); + + if let Ok(value) = result { + EraseMode::EraseFile(value) + } else { + warn!("No amount of passes provided - using the default."); + EraseMode::EraseFile(1) + } + } else { + EraseMode::IgnoreFile + }; + + let header_location = if sub_matches.is_present("header") { + HeaderLocation::Detached( + sub_matches + .value_of("header") + .context("No header/invalid text provided")? + .to_string(), + ) + } else { + HeaderLocation::Embedded + }; + + let hashing_algorithm = hashing_algorithm(sub_matches); + + Ok(CryptoParams { + hash_mode, + force, + erase, + key, + header_location, + hashing_algorithm, + }) +} + +pub fn hashing_algorithm(sub_matches: &ArgMatches) -> HashingAlgorithm { + if sub_matches.is_present("argon") { + HashingAlgorithm::Argon2id(ARGON2ID_LATEST) + } else { + HashingAlgorithm::Blake3Balloon(BLAKE3BALLOON_LATEST) + } +} + +// gets the algorithm, primarily for encrypt functions +pub fn algorithm(sub_matches: &ArgMatches) -> Algorithm { + if sub_matches.is_present("aes") { + Algorithm::Aes256Gcm + } else { + Algorithm::XChaCha20Poly1305 + } +} + +pub fn erase_params(sub_matches: &ArgMatches) -> Result<(i32, ForceMode)> { + let passes = if sub_matches.is_present("passes") { + let result = sub_matches + .value_of("passes") + .context("No amount of passes specified")? + .parse::(); + if let Ok(value) = result { + value + } else { + warn!("Unable to read number of passes provided - using the default."); + 1 + } + } else { + warn!("Number of passes not provided - using the default."); + 1 + }; + + let force = forcemode(sub_matches); + + Ok((passes, force)) +} + +pub fn pack_params(sub_matches: &ArgMatches) -> Result<(CryptoParams, PackParams)> { + let key = Key::init(sub_matches, &KeyParams::default(), "keyfile")?; + + let hash_mode = if sub_matches.is_present("hash") { + //specify to emit hash after operation + HashMode::CalculateHash + } else { + // default + HashMode::NoHash + }; + + let force = forcemode(sub_matches); + + let erase = EraseMode::IgnoreFile; + + let header_location = if sub_matches.is_present("header") { + HeaderLocation::Detached( + sub_matches + .value_of("header") + .context("No header/invalid text provided")? + .to_string(), + ) + } else { + HeaderLocation::Embedded + }; + + let hashing_algorithm = hashing_algorithm(sub_matches); + + let crypto_params = CryptoParams { + hash_mode, + force, + erase, + key, + header_location, + hashing_algorithm, + }; + + let print_mode = if sub_matches.is_present("verbose") { + //specify to emit hash after operation + PrintMode::Verbose + } else { + // default + PrintMode::Quiet + }; + + let dir_mode = if sub_matches.is_present("recursive") { + //specify to emit hash after operation + DirectoryMode::Recursive + } else { + // default + DirectoryMode::Singular + }; + + let erase_source = if sub_matches.is_present("erase") { + EraseSourceDir::Erase + } else { + EraseSourceDir::Retain + }; + + let compression = if sub_matches.is_present("zstd") { + Compression::Zstd + } else { + Compression::None + }; + + let pack_params = PackParams { + dir_mode, + print_mode, + erase_source, + compression, + }; + + Ok((crypto_params, pack_params)) +} + +pub fn forcemode(sub_matches: &ArgMatches) -> ForceMode { + if sub_matches.is_present("force") { + ForceMode::Force + } else { + ForceMode::Prompt + } +} + +pub fn key_manipulation_params(sub_matches: &ArgMatches) -> Result { + let key_old = Key::init( + sub_matches, + &KeyParams { + user: true, + env: false, + autogenerate: false, + keyfile: true, + }, + "keyfile-old", + )?; + + let key_new = Key::init( + sub_matches, + &KeyParams { + user: true, + env: false, + autogenerate: true, + keyfile: true, + }, + "keyfile-new", + )?; + + let hashing_algorithm = hashing_algorithm(sub_matches); + + Ok(KeyManipulationParams { + key_old, + key_new, + hashing_algorithm, + }) +} diff --git a/crates/volaris-cli/src/global/states.rs b/crates/volaris-cli/src/global/states.rs new file mode 100644 index 0000000..8b4ec8c --- /dev/null +++ b/crates/volaris-cli/src/global/states.rs @@ -0,0 +1,184 @@ +// this file contains enums found all around the codebase +// they act as toggles for certain features, so they can be +// enabled if selected by the user + +use anyhow::{Context, Result}; +use clap::ArgMatches; +use core::protected::Protected; + +use crate::cli::prompt::get_password; +use crate::warn; +use core::key::generate_passphrase; + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum DirectoryMode { + Singular, + Recursive, +} + +pub enum Compression { + None, + Zstd, +} + +#[derive(PartialEq, Eq)] +pub enum EraseSourceDir { + Erase, + Retain, +} + +#[derive(PartialEq, Eq)] +pub enum PrintMode { + Verbose, + Quiet, +} + +pub enum HeaderLocation { + Embedded, + Detached(String), +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum EraseMode { + EraseFile(i32), + IgnoreFile, +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum HashMode { + CalculateHash, + NoHash, +} + +#[derive(PartialEq, Eq, Copy, Clone)] +pub enum ForceMode { + Force, + Prompt, +} + +#[derive(PartialEq, Eq)] +pub enum Key { + Keyfile(String), + Env, + Generate(i32), + User, +} + +#[derive(PartialEq, Eq)] +pub enum PasswordState { + Validate, + Direct, // maybe not the best name +} + +fn get_bytes(reader: &mut R) -> Result>> { + let mut data = Vec::new(); + reader + .read_to_end(&mut data) + .context("Unable to read data")?; + Ok(Protected::new(data)) +} + +impl Key { + // this handles getting the secret, and returning it + // it relies on `parameters.rs`' handling and logic to determine which route to get the key + // it can handle keyfiles, env variables, automatically generating and letting the user enter a key + // it has a check for if the keyfile is empty or not + pub fn get_secret(&self, pass_state: &PasswordState) -> Result>> { + let secret = match self { + Key::Keyfile(path) if path == "-" => { + let mut reader = std::io::stdin(); + let secret = get_bytes(&mut reader)?; + if secret.is_empty() { + return Err(anyhow::anyhow!("STDIN is empty")); + } + secret + } + Key::Keyfile(path) => { + let mut reader = std::fs::File::open(path) + .with_context(|| format!("Unable to read file: {}", path))?; + let secret = get_bytes(&mut reader)?; + if secret.is_empty() { + return Err(anyhow::anyhow!(format!("Keyfile '{}' is empty", path))); + } + secret + } + Key::Env => Protected::new( + std::env::var("VOLARIS_KEY") + .context("Unable to read VOLARIS_KEY from environment variable")? + .into_bytes(), + ), + Key::User => get_password(pass_state)?, + Key::Generate(i) => { + let passphrase = generate_passphrase(i); + warn!("Your generated passphrase is: {}", passphrase.expose()); + let key = Protected::new(passphrase.expose().clone().into_bytes()); + drop(passphrase); + key + } + }; + + if secret.expose().is_empty() { + Err(anyhow::anyhow!("The specified key is empty!")) + } else { + Ok(secret) + } + } + + pub fn init( + sub_matches: &ArgMatches, + params: &KeyParams, + keyfile_descriptor: &str, + ) -> Result { + let key = if sub_matches.is_present(keyfile_descriptor) && params.keyfile { + Key::Keyfile( + sub_matches + .value_of(keyfile_descriptor) + .context("No keyfile/invalid text provided")? + .to_string(), + ) + } else if std::env::var("VOLARIS_KEY").is_ok() && params.env { + Key::Env + } else if let (Ok(true), true) = ( + sub_matches.try_contains_id("autogenerate"), + params.autogenerate, + ) { + let result = sub_matches + .value_of("autogenerate") + .context("No amount of words specified")? + .parse::(); + if let Ok(value) = result { + Key::Generate(value) + } else { + warn!("No amount of words specified - using the default."); + Key::Generate(7) + } + } else if params.user { + Key::User + } else { + return Err(anyhow::anyhow!( + "No key sources found with the parameters/arguments provided" + )); + }; + + Ok(key) + } +} + +#[allow(clippy::struct_excessive_bools)] +pub struct KeyParams { + pub user: bool, + pub env: bool, + pub autogenerate: bool, + pub keyfile: bool, +} + +impl KeyParams { + pub fn default() -> Self { + KeyParams { + user: true, + env: true, + autogenerate: true, + keyfile: true, + } + } +} diff --git a/crates/volaris-cli/src/global/structs.rs b/crates/volaris-cli/src/global/structs.rs new file mode 100644 index 0000000..15b3638 --- /dev/null +++ b/crates/volaris-cli/src/global/structs.rs @@ -0,0 +1,29 @@ +use core::header::HashingAlgorithm; + +use crate::global::states::{ForceMode, HashMode}; + +use super::states::{ + Compression, DirectoryMode, EraseMode, EraseSourceDir, HeaderLocation, Key, PrintMode, +}; + +pub struct CryptoParams { + pub hash_mode: HashMode, + pub force: ForceMode, + pub erase: EraseMode, + pub key: Key, + pub header_location: HeaderLocation, + pub hashing_algorithm: HashingAlgorithm, +} + +pub struct PackParams { + pub dir_mode: DirectoryMode, + pub print_mode: PrintMode, + pub erase_source: EraseSourceDir, + pub compression: Compression, +} + +pub struct KeyManipulationParams { + pub key_old: Key, + pub key_new: Key, + pub hashing_algorithm: HashingAlgorithm, +} diff --git a/crates/volaris-cli/src/main.rs b/crates/volaris-cli/src/main.rs index f328e4d..b3d6d1e 100644 --- a/crates/volaris-cli/src/main.rs +++ b/crates/volaris-cli/src/main.rs @@ -1 +1,69 @@ -fn main() {} +#![forbid(unsafe_code)] +#![warn(clippy::all)] + +use anyhow::Result; + +mod cli; +mod global; +mod subcommands; + +// this is where subcommand function calling is handled +// it goes hand-in-hand with `subcommands.rs` +// it works so that's good enough, and any changes are rather simple to make to it +// it handles the calling of other functions, and some (minimal) argument parsing +fn main() -> Result<()> { + let matches = cli::get_matches(); + + match matches.subcommand() { + Some(("encrypt", sub_matches)) => { + subcommands::encrypt(sub_matches)?; + } + Some(("decrypt", sub_matches)) => { + subcommands::decrypt(sub_matches)?; + } + Some(("erase", sub_matches)) => { + subcommands::erase(sub_matches)?; + } + Some(("pack", sub_matches)) => { + subcommands::pack(sub_matches)?; + } + Some(("unpack", sub_matches)) => { + subcommands::unpack(sub_matches)?; + } + Some(("hash", sub_matches)) => { + subcommands::hash_stream(sub_matches)?; + } + Some(("header", sub_matches)) => match sub_matches.subcommand_name() { + Some("dump") => { + subcommands::header_dump(sub_matches)?; + } + Some("restore") => { + subcommands::header_restore(sub_matches)?; + } + Some("strip") => { + subcommands::header_strip(sub_matches)?; + } + Some("details") => { + subcommands::header_details(sub_matches)?; + } + _ => (), + }, + Some(("key", sub_matches)) => match sub_matches.subcommand_name() { + Some("change") => { + subcommands::key_change(sub_matches)?; + } + Some("add") => { + subcommands::key_add(sub_matches)?; + } + Some("del") => { + subcommands::key_del(sub_matches)?; + } + Some("verify") => { + subcommands::key_verify(sub_matches)?; + } + _ => (), + }, + _ => (), + } + Ok(()) +} diff --git a/crates/volaris-cli/src/subcommands.rs b/crates/volaris-cli/src/subcommands.rs new file mode 100644 index 0000000..25c79f9 --- /dev/null +++ b/crates/volaris-cli/src/subcommands.rs @@ -0,0 +1,157 @@ +use anyhow::Result; +use clap::ArgMatches; + +// this is called from main.rs +// it gets params and sends them to the appropriate functions + +use crate::global::{ + parameters::{ + algorithm, erase_params, forcemode, get_param, get_params, key_manipulation_params, + pack_params, parameter_handler, + }, + states::{Key, KeyParams}, +}; + +pub mod decrypt; +pub mod encrypt; +pub mod erase; +pub mod hashing; +pub mod header; +pub mod key; +pub mod pack; +pub mod unpack; + +pub fn encrypt(sub_matches: &ArgMatches) -> Result<()> { + let params = parameter_handler(sub_matches)?; + let algorithm = algorithm(sub_matches); + + // stream mode is the only mode to encrypt (v8.5.0+) + encrypt::stream_mode( + &get_param("input", sub_matches)?, + &get_param("output", sub_matches)?, + ¶ms, + algorithm, + ) +} + +pub fn decrypt(sub_matches: &ArgMatches) -> Result<()> { + let params = parameter_handler(sub_matches)?; + + // stream decrypt is the default as it will redirect to memory mode if the header says so (for backwards-compat) + decrypt::stream_mode( + &get_param("input", sub_matches)?, + &get_param("output", sub_matches)?, + ¶ms, + ) +} + +pub fn erase(sub_matches: &ArgMatches) -> Result<()> { + let (passes, force) = erase_params(sub_matches)?; + + erase::secure_erase(&get_param("input", sub_matches)?, passes, force) +} + +pub fn pack(sub_matches: &ArgMatches) -> Result<()> { + let (crypto_params, pack_params) = pack_params(sub_matches)?; + let algorithm = algorithm(sub_matches); + + pack::execute(&pack::Request { + input_file: &get_params("input", sub_matches)?, + output_file: &get_param("output", sub_matches)?, + pack_params, + crypto_params, + algorithm, + }) +} + +pub fn unpack(sub_matches: &ArgMatches) -> Result<()> { + use super::global::states::PrintMode; + + let crypto_params = parameter_handler(sub_matches)?; + + let print_mode = if sub_matches.is_present("verbose") { + PrintMode::Verbose + } else { + PrintMode::Quiet + }; + + unpack::unpack( + &get_param("input", sub_matches)?, + &get_param("output", sub_matches)?, + print_mode, + crypto_params, + ) +} + +pub fn hash_stream(sub_matches: &ArgMatches) -> Result<()> { + let files: Vec = if sub_matches.is_present("input") { + let list: Vec<&str> = sub_matches.values_of("input").unwrap().collect(); + list.iter().map(std::string::ToString::to_string).collect() + } else { + Vec::new() + }; + + hashing::hash_stream(&files) +} + +pub fn header_dump(sub_matches: &ArgMatches) -> Result<()> { + let sub_matches_dump = sub_matches.subcommand_matches("dump").unwrap(); + let force = forcemode(sub_matches_dump); + + header::dump( + &get_param("input", sub_matches_dump)?, + &get_param("output", sub_matches_dump)?, + force, + ) +} + +pub fn header_restore(sub_matches: &ArgMatches) -> Result<()> { + let sub_matches_restore = sub_matches.subcommand_matches("restore").unwrap(); + + header::restore( + &get_param("input", sub_matches_restore)?, + &get_param("output", sub_matches_restore)?, + ) +} + +pub fn header_strip(sub_matches: &ArgMatches) -> Result<()> { + let sub_matches_strip = sub_matches.subcommand_matches("strip").unwrap(); + + header::strip(&get_param("input", sub_matches_strip)?) +} + +pub fn header_details(sub_matches: &ArgMatches) -> Result<()> { + let sub_matches_details = sub_matches.subcommand_matches("details").unwrap(); + + header::details(&get_param("input", sub_matches_details)?) +} + +pub fn key_change(sub_matches: &ArgMatches) -> Result<()> { + let sub_matches_change_key = sub_matches.subcommand_matches("change").unwrap(); + + let params = key_manipulation_params(sub_matches_change_key)?; + + key::change(&get_param("input", sub_matches_change_key)?, ¶ms) +} + +pub fn key_add(sub_matches: &ArgMatches) -> Result<()> { + let sub_matches_add_key = sub_matches.subcommand_matches("add").unwrap(); + + let params = key_manipulation_params(sub_matches_add_key)?; + + key::add(&get_param("input", sub_matches_add_key)?, ¶ms) +} + +pub fn key_del(sub_matches: &ArgMatches) -> Result<()> { + let sub_matches_del_key = sub_matches.subcommand_matches("del").unwrap(); + let key = Key::init(sub_matches_del_key, &KeyParams::default(), "keyfile")?; + + key::delete(&get_param("input", sub_matches_del_key)?, &key) +} + +pub fn key_verify(sub_matches: &ArgMatches) -> Result<()> { + let sub_matches_verify_key = sub_matches.subcommand_matches("verify").unwrap(); + let key = Key::init(sub_matches_verify_key, &KeyParams::default(), "keyfile")?; + + key::verify(&get_param("input", sub_matches_verify_key)?, &key) +} diff --git a/crates/volaris-cli/src/subcommands/decrypt.rs b/crates/volaris-cli/src/subcommands/decrypt.rs new file mode 100644 index 0000000..a413ecd --- /dev/null +++ b/crates/volaris-cli/src/subcommands/decrypt.rs @@ -0,0 +1,64 @@ +use std::process::exit; +use std::sync::Arc; + +use crate::cli::prompt::overwrite_check; +use crate::global::states::{EraseMode, HashMode, HeaderLocation, PasswordState}; +use crate::global::structs::CryptoParams; + +use anyhow::Result; + +use domain::storage::Storage; + +// this function is for decrypting a file in stream mode +// it handles any user-facing interactiveness, opening files, or redirecting to memory mode if +// the header says so (backwards-compat) +// it also manages using a detached header file if selected +// it creates the stream object and uses the convenience function provided by volaris-core +pub fn stream_mode(input: &str, output: &str, params: &CryptoParams) -> Result<()> { + // TODO: It is necessary to raise it to a higher level + let stor = Arc::new(domain::storage::FileStorage); + + // 1. validate and prepare options + if input == output { + return Err(anyhow::anyhow!( + "Input and output files cannot have the same name." + )); + } + + if !overwrite_check(output, params.force)? { + exit(0); + } + + let input_file = stor.read_file(input)?; + let header_file = match ¶ms.header_location { + HeaderLocation::Embedded => None, + HeaderLocation::Detached(path) => Some(stor.read_file(path)?), + }; + + let raw_key = params.key.get_secret(&PasswordState::Direct)?; + let output_file = stor + .create_file(output) + .or_else(|_| stor.write_file(output))?; + + // 2. decrypt file + domain::decrypt::execute(domain::decrypt::Request { + header_reader: header_file.as_ref().and_then(|h| h.try_reader().ok()), + reader: input_file.try_reader()?, + writer: output_file.try_writer()?, + raw_key, + on_decrypted_header: None, + })?; + + // 3. flush result + stor.flush_file(&output_file)?; + + if params.hash_mode == HashMode::CalculateHash { + super::hashing::hash_stream(&[input.to_string()])?; + } + + if let EraseMode::EraseFile(passes) = params.erase { + super::erase::secure_erase(input, passes, params.force)?; + } + + Ok(()) +} diff --git a/crates/volaris-cli/src/subcommands/encrypt.rs b/crates/volaris-cli/src/subcommands/encrypt.rs new file mode 100644 index 0000000..792cb5a --- /dev/null +++ b/crates/volaris-cli/src/subcommands/encrypt.rs @@ -0,0 +1,82 @@ +use crate::cli::prompt::overwrite_check; +use crate::global::states::{EraseMode, HashMode, HeaderLocation, PasswordState}; +use crate::global::structs::CryptoParams; +use anyhow::Result; +use core::header::{HeaderType, HEADER_VERSION}; +use core::primitives::{Algorithm, Mode}; +use std::process::exit; +use std::sync::Arc; + +use domain::storage::Storage; + +// this function is for encrypting a file in stream mode +// it handles any user-facing interactiveness, opening files +// it creates the stream object and uses the convenience function provided by volaris-core +pub fn stream_mode( + input: &str, + output: &str, + params: &CryptoParams, + algorithm: Algorithm, +) -> Result<()> { + // TODO: It is necessary to raise it to a higher level + let stor = Arc::new(domain::storage::FileStorage); + + // 1. validate and prepare options + if input == output { + return Err(anyhow::anyhow!( + "Input and output files cannot have the same name." + )); + } + + if !overwrite_check(output, params.force)? { + exit(0); + } + + let input_file = stor.read_file(input)?; + let raw_key = params.key.get_secret(&PasswordState::Validate)?; + let output_file = stor + .create_file(output) + .or_else(|_| stor.write_file(output))?; + + let header_file = match ¶ms.header_location { + HeaderLocation::Embedded => None, + HeaderLocation::Detached(path) => { + if !overwrite_check(path, params.force)? { + exit(0); + } + + Some(stor.create_file(path).or_else(|_| stor.write_file(path))?) + } + }; + + // 2. encrypt file + let req = domain::encrypt::Request { + reader: input_file.try_reader()?, + writer: output_file.try_writer()?, + header_writer: header_file.as_ref().and_then(|f| f.try_writer().ok()), + raw_key, + header_type: HeaderType { + version: HEADER_VERSION, + mode: Mode::StreamMode, + algorithm, + }, + hashing_algorithm: params.hashing_algorithm, + }; + domain::encrypt::execute(req)?; + + // 3. flush result + if let Some(header_file) = header_file { + stor.flush_file(&header_file)?; + } + stor.flush_file(&output_file)?; + + if params.hash_mode == HashMode::CalculateHash { + super::hashing::hash_stream(&[output.to_string()])?; + } + + if let EraseMode::EraseFile(passes) = params.erase { + super::erase::secure_erase(input, passes, params.force)?; + } + + Ok(()) +} diff --git a/crates/volaris-cli/src/subcommands/erase.rs b/crates/volaris-cli/src/subcommands/erase.rs new file mode 100644 index 0000000..0437c44 --- /dev/null +++ b/crates/volaris-cli/src/subcommands/erase.rs @@ -0,0 +1,47 @@ +use anyhow::Result; +use domain::storage::Storage; +use std::sync::Arc; + +use crate::global::states::ForceMode; + +use crate::cli::prompt::get_answer; + +// this function securely erases a file +// read the docs for some caveats with file-erasure on flash storage +// it takes the file name/relative path, and the number of times to go over the file's contents with random bytes +#[allow(clippy::module_name_repetitions)] +pub fn secure_erase(input: &str, passes: i32, force: ForceMode) -> Result<()> { + // TODO: It is necessary to raise it to a higher level + let stor = Arc::new(domain::storage::FileStorage); + + let file = stor.read_file(input)?; + if file.is_dir() + && !get_answer( + "This is a directory, would you like to erase all files within it?", + false, + force, + )? + { + std::process::exit(0); + } + + if file.is_dir() { + domain::erase_dir::execute( + stor, + domain::erase_dir::Request { + entry: file, + passes, + }, + )?; + } else { + domain::erase::execute( + stor, + domain::erase::Request { + path: input, + passes, + }, + )?; + } + + Ok(()) +} diff --git a/crates/volaris-cli/src/subcommands/hashing.rs b/crates/volaris-cli/src/subcommands/hashing.rs new file mode 100644 index 0000000..4d7e4a8 --- /dev/null +++ b/crates/volaris-cli/src/subcommands/hashing.rs @@ -0,0 +1,26 @@ +use anyhow::Context; +use anyhow::Result; +use std::cell::RefCell; + +use crate::success; + +// this hashes the input file +// it reads it in blocks, updates the hasher, and finalises/displays the hash +// it's used by hash-standalone mode +pub fn hash_stream(files: &[String]) -> Result<()> { + for input in files { + let mut input_file = std::fs::File::open(input) + .with_context(|| format!("Unable to open file: {}", input))?; + + let hash = domain::hash::execute( + domain::hasher::Blake3Hasher::default(), + domain::hash::Request { + reader: RefCell::new(&mut input_file), + }, + )?; + + success!("{}: {}", input, hash); + } + + Ok(()) +} diff --git a/crates/volaris-cli/src/subcommands/header.rs b/crates/volaris-cli/src/subcommands/header.rs new file mode 100644 index 0000000..8314253 --- /dev/null +++ b/crates/volaris-cli/src/subcommands/header.rs @@ -0,0 +1,139 @@ +use std::{ + cell::RefCell, + fs::{File, OpenOptions}, +}; + +use crate::cli::prompt::overwrite_check; +use crate::global::states::ForceMode; +use anyhow::{Context, Result}; +use core::header::HashingAlgorithm; +use core::header::{Header, HeaderVersion}; +use domain::storage::Storage; +use domain::utils::hex_encode; + +pub fn details(input: &str) -> Result<()> { + let mut input_file = + File::open(input).with_context(|| format!("Unable to open input file: {}", input))?; + + let header_result = Header::deserialize(&mut input_file); + + if header_result.is_err() { + return Err(anyhow::anyhow!( + "This does not seem like a valid Volaris header" + )); + } + + let (header, aad) = header_result.unwrap(); + + println!("Header version: {}", header.header_type.version); + println!("Encryption algorithm: {}", header.header_type.algorithm); + println!("Encryption mode: {}", header.header_type.mode); + println!("Encryption nonce: {} (hex)", hex_encode(&header.nonce)); + println!("AAD: {} (hex)", hex_encode(&aad)); + + match header.header_type.version { + HeaderVersion::V1 => { + println!("Salt: {} (hex)", hex_encode(&header.salt.unwrap())); + println!("Hashing Algorithm: {}", HashingAlgorithm::Argon2id(1)); + } + HeaderVersion::V2 => { + println!("Salt: {} (hex)", hex_encode(&header.salt.unwrap())); + println!("Hashing Algorithm: {}", HashingAlgorithm::Argon2id(2)); + } + HeaderVersion::V3 => { + println!("Salt: {} (hex)", hex_encode(&header.salt.unwrap())); + println!("Hashing Algorithm: {}", HashingAlgorithm::Argon2id(3)); + } + HeaderVersion::V4 | HeaderVersion::V5 => { + for (i, keyslot) in header.keyslots.unwrap().iter().enumerate() { + println!("Keyslot {}:", i); + println!(" Hashing Algorithm: {}", keyslot.hash_algorithm); + println!(" Salt: {} (hex)", hex_encode(&keyslot.salt)); + println!( + " Master Key: {} (hex, encrypted)", + hex_encode(&keyslot.encrypted_key) + ); + println!(" Master Key Nonce: {} (hex)", hex_encode(&keyslot.nonce)); + } + } + } + + Ok(()) +} + +// this function reads the header fromthe input file and writes it to the output file +// it's used for extracting an encrypted file's header for backups and such +// it implements a check to ensure the header is valid +pub fn dump(input: &str, output: &str, force: ForceMode) -> Result<()> { + let stor = std::sync::Arc::new(domain::storage::FileStorage); + let input_file = stor.read_file(input)?; + + if !overwrite_check(output, force)? { + std::process::exit(0); + } + + let output_file = stor + .create_file(output) + .or_else(|_| stor.write_file(output))?; + + let req = domain::header::dump::Request { + reader: input_file.try_reader()?, + writer: output_file.try_writer()?, + }; + + domain::header::dump::execute(req)?; + + stor.flush_file(&output_file)?; + + Ok(()) +} + +// this function reads the header from the input file +// it then writes the header to the start of the ouput file +// this can be used for restoring a dumped header to a file that had it's header stripped +// this does not work for files encrypted *with* a detached header +// it implements a check to ensure the header is valid before restoring to a file +pub fn restore(input: &str, output: &str) -> Result<()> { + let stor = std::sync::Arc::new(domain::storage::FileStorage); + + let input_file = stor.read_file(input)?; + + let output_file = RefCell::new( + OpenOptions::new() + .read(true) + .write(true) + .open(output) + .with_context(|| format!("Unable to open output file: {}", output))?, + ); + + let req = domain::header::restore::Request { + reader: input_file.try_reader()?, + writer: &output_file, + }; + + domain::header::restore::execute(req)?; + + Ok(()) +} + +// this wipes the length of the header from the provided file +// the header must be intact for this to work, as the length varies between the versions +// it can be useful for storing the header separate from the file, to make an attacker's life that little bit harder +// it implements a check to ensure the header is valid before stripping +pub fn strip(input: &str) -> Result<()> { + let input_file = RefCell::new( + OpenOptions::new() + .read(true) + .write(true) + .open(input) + .with_context(|| format!("Unable to open input file: {}", input))?, + ); + + let req = domain::header::strip::Request { + handle: &input_file, + }; + + domain::header::strip::execute(req)?; + + Ok(()) +} diff --git a/crates/volaris-cli/src/subcommands/key.rs b/crates/volaris-cli/src/subcommands/key.rs new file mode 100644 index 0000000..b2e9bc7 --- /dev/null +++ b/crates/volaris-cli/src/subcommands/key.rs @@ -0,0 +1,171 @@ +// TODO(brxken128): give this file a better name +use crate::global::states::Key; +use crate::global::states::PasswordState; +use crate::global::structs::KeyManipulationParams; +use anyhow::{Context, Result}; +use core::header::Header; +use core::header::HeaderVersion; +use std::cell::RefCell; +use std::fs::OpenOptions; +use std::io::Seek; + +use crate::info; + +pub fn add(input: &str, params: &KeyManipulationParams) -> Result<()> { + let input_file = RefCell::new( + OpenOptions::new() + .read(true) + .write(true) + .open(input) + .with_context(|| format!("Unable to open input file: {}", input))?, + ); + + let (header, _) = Header::deserialize(&mut *input_file.borrow_mut())?; + + if header.header_type.version < HeaderVersion::V5 { + return Err(anyhow::anyhow!( + "This function is not supported on header versions below V5" + )); + } + + input_file + .borrow_mut() + .rewind() + .context("Unable to rewind the reader")?; + + if params.key_old == Key::User { + info!("Please enter your old key below"); + } + + let raw_key_old = params.key_old.get_secret(&PasswordState::Direct)?; + + if params.key_new == Key::User { + info!("Please enter your new key below"); + } + + let raw_key_new = params.key_new.get_secret(&PasswordState::Validate)?; + + domain::key::add::execute(domain::key::add::Request { + handle: &input_file, + hash_algorithm: params.hashing_algorithm, + raw_key_old, + raw_key_new, + })?; + + Ok(()) +} + +pub fn change(input: &str, params: &KeyManipulationParams) -> Result<()> { + let input_file = RefCell::new( + OpenOptions::new() + .read(true) + .write(true) + .open(input) + .with_context(|| format!("Unable to open input file: {}", input))?, + ); + + let (header, _) = Header::deserialize(&mut *input_file.borrow_mut())?; + + if header.header_type.version < HeaderVersion::V5 { + return Err(anyhow::anyhow!( + "This function is not supported on header versions below V5" + )); + } + + input_file + .borrow_mut() + .rewind() + .context("Unable to rewind the reader")?; + + if params.key_old == Key::User { + info!("Please enter your old key below"); + } + + let raw_key_old = params.key_old.get_secret(&PasswordState::Direct)?; + + if params.key_new == Key::User { + info!("Please enter your new key below"); + } + + let raw_key_new = params.key_new.get_secret(&PasswordState::Validate)?; + + domain::key::change::execute(domain::key::change::Request { + handle: &input_file, + hash_algorithm: params.hashing_algorithm, + raw_key_old, + raw_key_new, + })?; + + Ok(()) +} + +pub fn delete(input: &str, key_old: &Key) -> Result<()> { + let input_file = RefCell::new( + OpenOptions::new() + .read(true) + .write(true) + .open(input) + .with_context(|| format!("Unable to open input file: {}", input))?, + ); + + let (header, _) = Header::deserialize(&mut *input_file.borrow_mut())?; + + if header.header_type.version < HeaderVersion::V5 { + return Err(anyhow::anyhow!( + "This function is not supported on header versions below V5" + )); + } + + input_file + .borrow_mut() + .rewind() + .context("Unable to rewind the reader")?; + + if key_old == &Key::User { + info!("Please enter your key below"); + } + + let raw_key_old = key_old.get_secret(&PasswordState::Direct)?; + + domain::key::delete::execute(domain::key::delete::Request { + handle: &input_file, + raw_key_old, + })?; + + Ok(()) +} + +pub fn verify(input: &str, key: &Key) -> Result<()> { + let input_file = RefCell::new( + OpenOptions::new() + .read(true) + .open(input) + .with_context(|| format!("Unable to open input file: {}", input))?, + ); + + let (header, _) = Header::deserialize(&mut *input_file.borrow_mut())?; + + if header.header_type.version < HeaderVersion::V5 { + return Err(anyhow::anyhow!( + "This function is not supported on header versions below V5" + )); + } + + input_file + .borrow_mut() + .rewind() + .context("Unable to rewind the reader")?; + + if key == &Key::User { + info!("Please enter your key below"); + } + + let raw_key = key.get_secret(&PasswordState::Direct)?; + + domain::key::verify::execute(domain::key::verify::Request { + handle: &input_file, + raw_key, + })?; + + Ok(()) +} diff --git a/crates/volaris-cli/src/subcommands/pack.rs b/crates/volaris-cli/src/subcommands/pack.rs new file mode 100644 index 0000000..bea3f2b --- /dev/null +++ b/crates/volaris-cli/src/subcommands/pack.rs @@ -0,0 +1,129 @@ +use std::path::PathBuf; +use std::process::exit; +use std::sync::Arc; + +use anyhow::Result; +use core::header::{HeaderType, HEADER_VERSION}; +use core::primitives::{Algorithm, Mode}; + +use crate::global::states::{HashMode, HeaderLocation, PasswordState}; +use crate::{ + global::states::EraseSourceDir, + global::{ + states::Compression, + structs::{CryptoParams, PackParams}, + }, +}; +use domain::storage::Storage; + +use crate::cli::prompt::overwrite_check; + +pub struct Request<'a> { + pub input_file: &'a Vec, + pub output_file: &'a str, + pub pack_params: PackParams, + pub crypto_params: CryptoParams, + pub algorithm: Algorithm, +} + +// this first indexes the input directory +// once it has the total number of files/folders, it creates a temporary zip file +// it compresses all of the files into the temporary archive +// once compressed, it encrypts the zip file +// it erases the temporary archive afterwards, to stop any residual data from remaining +pub fn execute(req: &Request) -> Result<()> { + // TODO: It is necessary to raise it to a higher level + let stor = Arc::new(domain::storage::FileStorage); + + // 1. validate and prepare options + if req.input_file.iter().any(|f| f == req.output_file) { + return Err(anyhow::anyhow!( + "Input and output files cannot have the same name." + )); + } + + if req.input_file.iter().any(|f| PathBuf::from(f).is_file()) { + return Err(anyhow::anyhow!("Input path cannot be a file.")); + } + + if !overwrite_check(req.output_file, req.crypto_params.force)? { + exit(0); + } + + let input_files = req + .input_file + .iter() + .map(|file_name| stor.read_file(file_name)) + .collect::, _>>()?; + let raw_key = req.crypto_params.key.get_secret(&PasswordState::Validate)?; + let output_file = stor + .create_file(req.output_file) + .or_else(|_| stor.write_file(req.output_file))?; + + let header_file = match &req.crypto_params.header_location { + HeaderLocation::Embedded => None, + HeaderLocation::Detached(path) => { + if !overwrite_check(path, req.crypto_params.force)? { + exit(0); + } + + Some(stor.create_file(path).or_else(|_| stor.write_file(path))?) + } + }; + + let compress_files = input_files + .into_iter() + .flat_map(|file| { + if file.is_dir() { + // TODO(pleshevskiy): use iterator instead of vec! + match stor.read_dir(&file) { + Ok(files) => files.into_iter().map(Ok).collect(), + Err(err) => vec![Err(err)], + } + } else { + vec![Ok(file)] + } + }) + .collect::, _>>()?; + + let compression_method = match req.pack_params.compression { + Compression::None => zip::CompressionMethod::Stored, + Compression::Zstd => zip::CompressionMethod::Zstd, + }; + + // 2. compress and encrypt files + domain::pack::execute( + stor.clone(), + domain::pack::Request { + compress_files, + compression_method, + writer: output_file.try_writer()?, + header_writer: header_file.as_ref().and_then(|f| f.try_writer().ok()), + raw_key, + header_type: HeaderType { + version: HEADER_VERSION, + mode: Mode::StreamMode, + algorithm: req.algorithm, + }, + hashing_algorithm: req.crypto_params.hashing_algorithm, + }, + )?; + + // 3. flush result + if let Some(header_file) = header_file { + stor.flush_file(&header_file)?; + } + stor.flush_file(&output_file)?; + + if req.crypto_params.hash_mode == HashMode::CalculateHash { + super::hashing::hash_stream(&[req.output_file.to_string()])?; + } + + if req.pack_params.erase_source == EraseSourceDir::Erase { + req.input_file.iter().try_for_each(|file_name| { + super::erase::secure_erase(file_name, 1, req.crypto_params.force) + })?; + } + + Ok(()) +} diff --git a/crates/volaris-cli/src/subcommands/unpack.rs b/crates/volaris-cli/src/subcommands/unpack.rs new file mode 100644 index 0000000..a83ea1a --- /dev/null +++ b/crates/volaris-cli/src/subcommands/unpack.rs @@ -0,0 +1,81 @@ +use crate::{cli::prompt::get_answer, global::states::HashMode}; +use std::sync::Arc; + +use anyhow::Result; + +use domain::storage::Storage; + +use crate::global::{ + states::{HeaderLocation, PasswordState, PrintMode}, + structs::CryptoParams, +}; +use crate::{info, warn}; +use std::path::PathBuf; + +// this first decrypts the input file to a temporary zip file +// it then unpacks that temporary zip file to the target directory +// once finished, it erases the temporary file to avoid any residual data +#[allow(clippy::module_name_repetitions)] +#[allow(clippy::needless_pass_by_value)] +pub fn unpack( + input: &str, // encrypted zip file + output: &str, // directory + print_mode: PrintMode, + params: CryptoParams, // params for decrypt function +) -> Result<()> { + // TODO: It is necessary to raise it to a higher level + let stor = Arc::new(domain::storage::FileStorage); + + let input_file = stor.read_file(input)?; + let header_file = match ¶ms.header_location { + HeaderLocation::Embedded => None, + HeaderLocation::Detached(path) => Some(stor.read_file(path)?), + }; + + let raw_key = params.key.get_secret(&PasswordState::Direct)?; + + domain::unpack::execute( + stor, + domain::unpack::Request { + header_reader: header_file.as_ref().and_then(|h| h.try_reader().ok()), + reader: input_file.try_reader()?, + output_dir_path: PathBuf::from(output), + raw_key, + on_decrypted_header: None, + on_archive_info: None, + on_zip_file: Some(Box::new(move |file_path| { + let file_name = file_path + .file_name() + .expect("Unable to convert file name to OsStr") + .to_str() + .expect("Unable to convert file name's OsStr to &str") + .to_string(); + + if std::fs::metadata(file_path).is_ok() { + let answer = get_answer( + &format!("{} already exists, would you like to overwrite?", file_name), + true, + params.force, + ) + .expect("Unable to read answer"); + if !answer { + warn!("Skipping {}", file_name); + return false; + } + } + + if print_mode == PrintMode::Verbose { + info!("Extracting {}", file_name); + } + + true + })), + }, + )?; + + if params.hash_mode == HashMode::CalculateHash { + super::hashing::hash_stream(&[input.to_string()])?; + } + + Ok(()) +} diff --git a/crates/volaris-core/README.md b/crates/volaris-core/README.md index 725a0a2..c213c56 100644 --- a/crates/volaris-core/README.md +++ b/crates/volaris-core/README.md @@ -16,12 +16,10 @@ own risk ## Who uses volaris-Core? -This library is implemented by [volaris](https://github.com/volar-is/volaris), a -secure multi-interface file encryption utility. +This library is implemented by [volaris](https://github.com/volarisapp/volaris), a secure multi-interface file encryption utility. Volaris-Core makes it easy to integrate the volaris format into your own projects -(and if there's a feature that you'd like to see, please don't hesitate to -[open a Github issue](https://github.com/volar-is/volaris/issues)). +(and if there's a feature that you'd like to see, please don't hesitate to [open a Github issue](https://github.com/volarisapp/volaris/issues)). ## Features @@ -78,8 +76,7 @@ let decrypted_data = cipher.decrypt(&nonce, encrypted_data.as_slice()).unwrap(); assert_eq!(secret, decrypted_data); ``` -You can read more about volaris, Volaris-Core and the technical details -[in the project's main documentation](https://github.com/volar-is/volaris/)! +You can read more about volaris, Volaris-Core and the technical details [in the project's main documentation](https://github.com/volarisapp/volaris/)! ## Thank you! diff --git a/crates/volaris-domain/Cargo.toml b/crates/volaris-domain/Cargo.toml new file mode 100644 index 0000000..7953122 --- /dev/null +++ b/crates/volaris-domain/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "volaris-domain" +description = "The libary for the workings of Volaris" +version = "0.0.1" +edition = "2021" +license = "BSD-2-Clause" +keywords = ["encryption", "secure"] +categories = ["cryptography", "encoding", "data-structures"] +repository = "https://github.com/volar-is/volaris/tree/master/crates/volaris-domain" +homepage = "https://github.com/volar-is/volaris" +readme = "README.md" +authors = ["brxken128 "] + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] +core = { package = "volaris-core", path = "../volaris-core", version = "0.0.1" } + +rand = "0.8.5" +blake3 = "1.3.3" +walkdir = "2.3.2" +zip = { version = "0.6.3", default-features = false, features = ["zstd"] } diff --git a/crates/volaris-domain/README.md b/crates/volaris-domain/README.md new file mode 100644 index 0000000..6f0449f --- /dev/null +++ b/crates/volaris-domain/README.md @@ -0,0 +1 @@ +## Soon \ No newline at end of file diff --git a/crates/volaris-domain/src/decrypt.rs b/crates/volaris-domain/src/decrypt.rs new file mode 100644 index 0000000..f55a992 --- /dev/null +++ b/crates/volaris-domain/src/decrypt.rs @@ -0,0 +1,263 @@ +//! This provides functionality for decryption that adheres to the volaris format. + +use std::cell::RefCell; +use std::io::{Read, Seek, Write}; + +use core::cipher::Ciphers; +use core::header::{Header, HeaderType}; +use core::key::decrypt_master_key; +use core::primitives::Mode; +use core::protected::Protected; +use core::stream::DecryptionStreams; + +#[derive(Debug)] +pub enum Error { + InitializeChiphers, + InitializeStreams, + DeserializeHeader, + ReadEncryptedData, + DecryptMasterKey, + DecryptData, + WriteData, + RewindDataReader, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InitializeChiphers => f.write_str("Cannot initialize chiphers"), + Error::InitializeStreams => f.write_str("Cannot initialize streams"), + Error::DeserializeHeader => f.write_str("Cannot deserialize header"), + Error::ReadEncryptedData => f.write_str("Unable to read encrypted data"), + Error::DecryptMasterKey => f.write_str("Cannot decrypt master key"), + Error::DecryptData => f.write_str("Unable to decrypt data"), + Error::WriteData => f.write_str("Unable to write data"), + Error::RewindDataReader => f.write_str("Unable to rewind the reader"), + } + } +} + +impl std::error::Error for Error {} + +pub type OnDecryptedHeaderFn = Box; + +pub struct Request<'a, R, W> +where + R: Read + Seek, + W: Write + Seek, +{ + pub header_reader: Option<&'a RefCell>, + pub reader: &'a RefCell, + pub writer: &'a RefCell, + pub raw_key: Protected>, + pub on_decrypted_header: Option, +} + +pub fn execute(req: Request<'_, R, W>) -> Result<(), Error> +where + R: Read + Seek, + W: Write + Seek, +{ + let (header, aad) = match req.header_reader { + Some(header_reader) => { + let (header, aad) = Header::deserialize(&mut *header_reader.borrow_mut()) + .map_err(|_| Error::DeserializeHeader)?; + + // Try reading an empty header from the content. + #[allow(clippy::cast_possible_truncation)] + let mut header_bytes = vec![0u8; header.get_size() as usize]; + + req.reader + .borrow_mut() + .read_exact(&mut header_bytes) + .or_else(|e| { + if e.kind() == std::io::ErrorKind::UnexpectedEof { + Ok(()) + } else { + Err(e) + } + }) + .map_err(|_| Error::ReadEncryptedData)?; + + if !header_bytes.into_iter().all(|b| b == 0) { + // And return the cursor position to the start if it wasn't found + req.reader + .borrow_mut() + .rewind() + .map_err(|_| Error::RewindDataReader)?; + } + + (header, aad) + } + None => Header::deserialize(&mut *req.reader.borrow_mut()) + .map_err(|_| Error::DeserializeHeader)?, + }; + + if let Some(cb) = req.on_decrypted_header { + cb(&header.header_type); + } + + match header.header_type.mode { + Mode::MemoryMode => { + let mut encrypted_data = Vec::new(); + req.reader + .borrow_mut() + .read_to_end(&mut encrypted_data) + .map_err(|_| Error::ReadEncryptedData)?; + + let master_key = + decrypt_master_key(req.raw_key, &header).map_err(|_| Error::DecryptMasterKey)?; + + let ciphers = Ciphers::initialize(master_key, &header.header_type.algorithm) + .map_err(|_| Error::InitializeChiphers)?; + + let payload = core::Payload { + aad: &aad, + msg: &encrypted_data, + }; + + let decrypted_bytes = ciphers + .decrypt(&header.nonce, payload) + .map_err(|_| Error::DecryptData)?; + + req.writer + .borrow_mut() + .write_all(&decrypted_bytes) + .map_err(|_| Error::WriteData)?; + } + Mode::StreamMode => { + let master_key = + decrypt_master_key(req.raw_key, &header).map_err(|_| Error::DecryptMasterKey)?; + + let streams = DecryptionStreams::initialize( + master_key, + &header.nonce, + &header.header_type.algorithm, + ) + .map_err(|_| Error::InitializeStreams)?; + + streams + .decrypt_file( + &mut *req.reader.borrow_mut(), + &mut *req.writer.borrow_mut(), + &aad, + ) + .map_err(|_| Error::DecryptData)?; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + use crate::encrypt::tests::{ + PASSWORD, V4_ENCRYPTED_CONTENT, V5_ENCRYPTED_CONTENT, V5_ENCRYPTED_DETACHED_CONTENT, + V5_ENCRYPTED_DETACHED_HEADER, V5_ENCRYPTED_FULL_DETACHED_CONTENT, + }; + + #[test] + fn should_decrypt_encrypted_content_with_v4_version() { + let mut input_content = V4_ENCRYPTED_CONTENT.to_vec(); + let input_cur = RefCell::new(Cursor::new(&mut input_content)); + + let mut output_content = vec![]; + let output_cur = RefCell::new(Cursor::new(&mut output_content)); + + let req = Request { + header_reader: None, + reader: &input_cur, + writer: &output_cur, + raw_key: Protected::new(PASSWORD.to_vec()), + on_decrypted_header: None, + }; + + match execute(req) { + Ok(_) => { + assert_eq!(output_content, "Hello world".as_bytes().to_vec()); + } + _ => unreachable!(), + } + } + + #[test] + fn should_decrypt_encrypted_content_with_v5_version() { + let mut input_content = V5_ENCRYPTED_CONTENT.to_vec(); + let input_cur = RefCell::new(Cursor::new(&mut input_content)); + + let mut output_content = vec![]; + let output_cur = RefCell::new(Cursor::new(&mut output_content)); + + let req = Request { + header_reader: None, + reader: &input_cur, + writer: &output_cur, + raw_key: Protected::new(PASSWORD.to_vec()), + on_decrypted_header: None, + }; + + match execute(req) { + Ok(_) => { + assert_eq!(output_content, "Hello world".as_bytes().to_vec()); + } + _ => unreachable!(), + } + } + + #[test] + fn should_decrypt_encrypted_detached_header_and_content_with_v5_version() { + let mut input_content = V5_ENCRYPTED_DETACHED_CONTENT.to_vec(); + let input_cur = RefCell::new(Cursor::new(&mut input_content)); + + let mut input_header = V5_ENCRYPTED_DETACHED_HEADER.to_vec(); + let header_cur = RefCell::new(Cursor::new(&mut input_header)); + + let mut output_content = vec![]; + let output_cur = RefCell::new(Cursor::new(&mut output_content)); + + let req = Request { + header_reader: Some(&header_cur), + reader: &input_cur, + writer: &output_cur, + raw_key: Protected::new(PASSWORD.to_vec()), + on_decrypted_header: None, + }; + + match execute(req) { + Ok(_) => { + assert_eq!(output_content, "Hello world".as_bytes().to_vec()); + } + _ => unreachable!(), + } + } + + #[test] + fn should_decrypt_encrypted_full_detached_header_and_content_with_v5_version() { + let mut input_content = V5_ENCRYPTED_FULL_DETACHED_CONTENT.to_vec(); + let input_cur = RefCell::new(Cursor::new(&mut input_content)); + + let mut input_header = V5_ENCRYPTED_DETACHED_HEADER.to_vec(); + let header_cur = RefCell::new(Cursor::new(&mut input_header)); + + let mut output_content = vec![]; + let output_cur = RefCell::new(Cursor::new(&mut output_content)); + + let req = Request { + header_reader: Some(&header_cur), + reader: &input_cur, + writer: &output_cur, + raw_key: Protected::new(PASSWORD.to_vec()), + on_decrypted_header: None, + }; + + match execute(req) { + Ok(_) => { + assert_eq!(output_content, "Hello world".as_bytes().to_vec()); + } + _ => unreachable!(), + } + } +} diff --git a/crates/volaris-domain/src/encrypt.rs b/crates/volaris-domain/src/encrypt.rs new file mode 100644 index 0000000..8cac8af --- /dev/null +++ b/crates/volaris-domain/src/encrypt.rs @@ -0,0 +1,337 @@ +//! This provides functionality for encryption that adheres to the volaris format. + +use std::cell::RefCell; +use std::io::{Read, Seek, Write}; + +use core::cipher::Ciphers; +use core::header::{HashingAlgorithm, Header, HeaderType, Keyslot}; +use core::primitives::{Mode, ENCRYPTED_MASTER_KEY_LEN}; +use core::protected::Protected; +use core::stream::EncryptionStreams; + +use crate::utils::{gen_master_key, gen_nonce, gen_salt}; + +#[derive(Debug)] +pub enum Error { + ResetCursorPosition, + HashKey, + EncryptMasterKey, + EncryptFile, + WriteHeader, + InitializeStreams, + InitializeChiphers, + CreateAad, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ResetCursorPosition => f.write_str("Unable to reset cursor position"), + Error::HashKey => f.write_str("Cannot hash raw key"), + Error::EncryptMasterKey => f.write_str("Cannot encrypt master key"), + Error::EncryptFile => f.write_str("Cannot encrypt file"), + Error::WriteHeader => f.write_str("Cannot write header"), + Error::InitializeStreams => f.write_str("Cannot initialize streams"), + Error::InitializeChiphers => f.write_str("Cannot initialize chiphers"), + Error::CreateAad => f.write_str("Cannot create AAD"), + } + } +} + +impl std::error::Error for Error {} + +pub struct Request<'a, R, W> +where + R: Read + Seek, + W: Write + Seek, +{ + pub reader: &'a RefCell, + pub writer: &'a RefCell, + pub header_writer: Option<&'a RefCell>, + pub raw_key: Protected>, + // TODO: don't use external types in logic + pub header_type: HeaderType, + pub hashing_algorithm: HashingAlgorithm, +} + +pub fn execute(req: Request<'_, R, W>) -> Result<(), Error> +where + R: Read + Seek, + W: Write + Seek, +{ + // 1. generate salt + let salt = gen_salt(); + + // 2. hash key + let key = req + .hashing_algorithm + .hash(req.raw_key, &salt) + .map_err(|_| Error::HashKey)?; + + // 3. initialize cipher + let cipher = Ciphers::initialize(key, &req.header_type.algorithm) + .map_err(|_| Error::InitializeChiphers)?; + + // 4. generate master key + let master_key = gen_master_key(); + + let master_key_nonce = gen_nonce(&req.header_type.algorithm, &Mode::MemoryMode); + + // 5. encrypt master key + let master_key_encrypted = { + let encrypted_key = cipher + .encrypt(master_key_nonce.as_slice(), master_key.as_slice()) + .map_err(|_| Error::EncryptMasterKey)?; + + let mut encrypted_key_arr = [0u8; ENCRYPTED_MASTER_KEY_LEN]; + let len = ENCRYPTED_MASTER_KEY_LEN.min(encrypted_key.len()); + encrypted_key_arr[..len].copy_from_slice(&encrypted_key[..len]); + + encrypted_key_arr + }; + + let keyslot = Keyslot { + encrypted_key: master_key_encrypted, + nonce: master_key_nonce, + hash_algorithm: req.hashing_algorithm, + salt, + }; + + let keyslots = vec![keyslot]; + + let header_nonce = gen_nonce(&req.header_type.algorithm, &req.header_type.mode); + let streams = + EncryptionStreams::initialize(master_key, &header_nonce, &req.header_type.algorithm) + .map_err(|_| Error::InitializeStreams)?; + + let header = Header { + header_type: req.header_type, + nonce: header_nonce, + salt: None, + keyslots: Some(keyslots), + }; + + req.writer + .borrow_mut() + .rewind() + .map_err(|_| Error::ResetCursorPosition)?; + + match req.header_writer { + None => { + req.writer + .borrow_mut() + .write(&header.serialize().map_err(|_| Error::WriteHeader)?) + .map_err(|_| Error::WriteHeader)?; + } + Some(header_writer) => { + header_writer + .borrow_mut() + .rewind() + .map_err(|_| Error::ResetCursorPosition)?; + + header_writer + .borrow_mut() + .write(&header.serialize().map_err(|_| Error::WriteHeader)?) + .map_err(|_| Error::WriteHeader)?; + } + } + + let aad = header.create_aad().map_err(|_| Error::CreateAad)?; + + let mut reader = req.reader.borrow_mut(); + reader.rewind().map_err(|_| Error::ResetCursorPosition)?; + + let mut writer = req.writer.borrow_mut(); + streams + .encrypt_file(&mut *reader, &mut *writer, &aad) + .map_err(|_| Error::EncryptFile)?; + + Ok(()) +} + +// WARNING! Very expensive tests! +// TODO(pleshevskiy): think about optimizations +#[cfg(test)] +pub mod tests { + use std::io::Cursor; + + use core::header::HeaderVersion; + use core::primitives::Algorithm; + + use super::*; + + pub const PASSWORD: &[u8; 8] = b"12345678"; + + pub const V4_ENCRYPTED_CONTENT: [u8; 155] = [ + 222, 4, 14, 1, 12, 1, 58, 206, 16, 183, 233, 128, 23, 223, 81, 30, 214, 132, 32, 104, 51, + 119, 173, 240, 60, 45, 230, 243, 58, 160, 69, 50, 217, 192, 66, 223, 124, 190, 148, 91, 92, + 129, 0, 0, 0, 0, 0, 0, 147, 32, 67, 18, 249, 211, 189, 86, 187, 159, 234, 160, 94, 80, 72, + 68, 231, 114, 132, 105, 164, 177, 26, 217, 46, 168, 97, 110, 34, 27, 13, 16, 14, 111, 3, + 109, 218, 232, 212, 78, 188, 55, 91, 106, 97, 74, 238, 210, 173, 240, 60, 45, 230, 243, 58, + 160, 69, 50, 217, 192, 66, 223, 124, 190, 148, 91, 92, 129, 50, 126, 110, 254, 0, 0, 0, 0, + 0, 0, 0, 0, 14, 110, 105, 217, 74, 171, 173, 103, 11, 136, 119, 98, 145, 17, 70, 84, 144, + 143, 154, 244, 82, 201, 85, 13, 187, 85, 89, + ]; + + pub const V5_ENCRYPTED_CONTENT: [u8; 443] = [ + 222, 5, 14, 1, 12, 1, 173, 240, 60, 45, 230, 243, 58, 160, 69, 50, 217, 192, 66, 223, 124, + 190, 148, 91, 92, 129, 0, 0, 0, 0, 0, 0, 223, 181, 71, 240, 140, 106, 41, 36, 82, 150, 105, + 215, 159, 108, 234, 246, 25, 19, 65, 206, 177, 146, 15, 174, 209, 129, 82, 2, 62, 76, 129, + 34, 136, 189, 11, 98, 105, 54, 146, 71, 102, 166, 97, 177, 207, 62, 194, 132, 38, 87, 173, + 240, 60, 45, 230, 243, 58, 160, 69, 50, 217, 192, 66, 223, 124, 190, 148, 91, 92, 129, 50, + 126, 110, 254, 58, 206, 16, 183, 233, 128, 23, 223, 81, 30, 214, 132, 32, 104, 51, 119, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 110, 105, 217, 74, + 171, 173, 103, 11, 136, 119, 172, 145, 72, 239, 74, 217, 63, 245, 222, 31, 164, 139, 146, + 71, 165, 91, + ]; + + pub const V5_ENCRYPTED_FULL_DETACHED_CONTENT: [u8; 27] = [ + 14, 110, 105, 217, 74, 171, 173, 103, 11, 136, 119, 172, 145, 72, 239, 74, 217, 63, 245, + 222, 31, 164, 139, 146, 71, 165, 91, + ]; + pub const V5_ENCRYPTED_DETACHED_CONTENT: [u8; 443] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 14, 110, 105, + 217, 74, 171, 173, 103, 11, 136, 119, 172, 145, 72, 239, 74, 217, 63, 245, 222, 31, 164, + 139, 146, 71, 165, 91, + ]; + pub const V5_ENCRYPTED_DETACHED_HEADER: [u8; 416] = [ + 222, 5, 14, 1, 12, 1, 173, 240, 60, 45, 230, 243, 58, 160, 69, 50, 217, 192, 66, 223, 124, + 190, 148, 91, 92, 129, 0, 0, 0, 0, 0, 0, 223, 181, 71, 240, 140, 106, 41, 36, 82, 150, 105, + 215, 159, 108, 234, 246, 25, 19, 65, 206, 177, 146, 15, 174, 209, 129, 82, 2, 62, 76, 129, + 34, 136, 189, 11, 98, 105, 54, 146, 71, 102, 166, 97, 177, 207, 62, 194, 132, 38, 87, 173, + 240, 60, 45, 230, 243, 58, 160, 69, 50, 217, 192, 66, 223, 124, 190, 148, 91, 92, 129, 50, + 126, 110, 254, 58, 206, 16, 183, 233, 128, 23, 223, 81, 30, 214, 132, 32, 104, 51, 119, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + #[test] + fn should_encrypt_content_with_v4_version() { + let mut input_content = b"Hello world"; + let input_cur = RefCell::new(Cursor::new(&mut input_content)); + + let mut output_content = vec![]; + let output_cur = RefCell::new(Cursor::new(&mut output_content)); + + let req = Request { + reader: &input_cur, + writer: &output_cur, + header_writer: None, + raw_key: Protected::new(PASSWORD.to_vec()), + header_type: HeaderType { + version: HeaderVersion::V4, + algorithm: Algorithm::XChaCha20Poly1305, + mode: Mode::StreamMode, + }, + hashing_algorithm: HashingAlgorithm::Blake3Balloon(4), + }; + + match execute(req) { + Ok(_) => { + assert_eq!(output_content, V4_ENCRYPTED_CONTENT.to_vec()); + } + Err(e) => { + println!("{e:?}"); + unreachable!() + } + } + } + + #[test] + fn should_encrypt_content_with_v5_version() { + let mut input_content = b"Hello world"; + let input_cur = RefCell::new(Cursor::new(&mut input_content)); + + let mut output_content = vec![]; + let output_cur = RefCell::new(Cursor::new(&mut output_content)); + + let req = Request { + reader: &input_cur, + writer: &output_cur, + header_writer: None, + raw_key: Protected::new(PASSWORD.to_vec()), + header_type: HeaderType { + version: HeaderVersion::V5, + algorithm: Algorithm::XChaCha20Poly1305, + mode: Mode::StreamMode, + }, + hashing_algorithm: HashingAlgorithm::Blake3Balloon(5), + }; + + match execute(req) { + Ok(_) => { + assert_eq!(output_content, V5_ENCRYPTED_CONTENT.to_vec()); + } + Err(e) => { + println!("{e:?}"); + unreachable!() + } + } + } + + #[test] + fn should_save_header_separately() { + let mut input_content = b"Hello world"; + let input_cur = RefCell::new(Cursor::new(&mut input_content)); + + let mut output_content = vec![]; + let output_cur = RefCell::new(Cursor::new(&mut output_content)); + + let mut output_header = vec![]; + let output_header_cur = RefCell::new(Cursor::new(&mut output_header)); + + let req = Request { + reader: &input_cur, + writer: &output_cur, + header_writer: Some(&output_header_cur), + raw_key: Protected::new(PASSWORD.to_vec()), + header_type: HeaderType { + version: HeaderVersion::V5, + algorithm: Algorithm::XChaCha20Poly1305, + mode: Mode::StreamMode, + }, + hashing_algorithm: HashingAlgorithm::Blake3Balloon(5), + }; + + match execute(req) { + Ok(_) => { + assert_eq!(output_content, V5_ENCRYPTED_FULL_DETACHED_CONTENT.to_vec()); + assert_eq!(output_header, V5_ENCRYPTED_DETACHED_HEADER.to_vec()); + } + Err(e) => { + println!("{e:?}"); + unreachable!() + } + } + } +} diff --git a/crates/volaris-domain/src/erase.rs b/crates/volaris-domain/src/erase.rs new file mode 100644 index 0000000..8dd6404 --- /dev/null +++ b/crates/volaris-domain/src/erase.rs @@ -0,0 +1,93 @@ +//! This provides functionality for "shredding" a file. +//! +//! This will not be effective on flash storage, and if you are planning to release a program that uses this function, I'd recommend putting the default number of passes to 1. + +use std::io::{Read, Seek, Write}; +use std::path::Path; +use std::sync::Arc; + +use crate::storage::Storage; + +#[derive(Debug)] +pub enum Error { + OpenFile, + Overwrite(crate::overwrite::Error), + RemoveFile, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::OpenFile => f.write_str("Unable to open file"), + Error::Overwrite(inner) => write!(f, "Unable to overwrite file: {inner}"), + Error::RemoveFile => f.write_str("Unable to remove file"), + } + } +} + +impl std::error::Error for Error {} + +pub struct Request> { + pub path: P, + pub passes: i32, +} + +pub fn execute(stor: Arc + 'static>, req: Request

) -> Result<(), Error> +where + RW: Read + Write + Seek, + P: AsRef, +{ + let file = stor.write_file(req.path).map_err(|_| Error::OpenFile)?; + let buf_capacity = stor.file_len(&file).map_err(|_| Error::OpenFile)?; + + crate::overwrite::execute(crate::overwrite::Request { + writer: file + .try_writer() + .expect("We're confident that we're in writing mode"), + buf_capacity, + passes: req.passes, + }) + .map_err(Error::Overwrite)?; + + stor.remove_file(file).map_err(|_| Error::RemoveFile)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use crate::storage::InMemoryStorage; + + use super::*; + + #[test] + fn should_erase_file() { + let stor = Arc::new(InMemoryStorage::default()); + stor.add_hello_txt(); + + let req = Request { + path: "hello.txt", + passes: 2, + }; + match execute(stor.clone(), req) { + Ok(_) => assert_eq!(stor.files().get(&PathBuf::from("hello.txt")), None), + _ => unreachable!(), + } + } + + #[test] + fn should_not_open_file() { + let stor = Arc::new(InMemoryStorage::default()); + + let req = Request { + path: "hello.txt", + passes: 2, + }; + match execute(stor, req) { + Err(Error::OpenFile) => {} + _ => unreachable!(), + } + } +} diff --git a/crates/volaris-domain/src/erase_dir.rs b/crates/volaris-domain/src/erase_dir.rs new file mode 100644 index 0000000..b8f3e3b --- /dev/null +++ b/crates/volaris-domain/src/erase_dir.rs @@ -0,0 +1,109 @@ +//! This provides functionality for "shredding" a directory. It first traverses the directory, and then calls `shred` on all files. +//! +//! This will not be effective on flash storage, and if you are planning to release a program that uses this function, I'd recommend putting the default number of passes to 1. + +use std::io::{Read, Seek, Write}; +use std::sync::Arc; + +use crate::storage::Storage; + +#[derive(Debug)] +pub enum Error { + InvalidFileType, + EraseFile(crate::erase::Error), + ReadDirEntries, + RemoveDir, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InvalidFileType => f.write_str("Invalid file type"), + Error::EraseFile(inner) => write!(f, "Unable to erase file: {inner}"), + Error::ReadDirEntries => f.write_str("Unable to get all dir entries"), + Error::RemoveDir => f.write_str("Unable to remove directory recursively"), + } + } +} + +impl std::error::Error for Error {} + +pub struct Request +where + RW: Read + Write + Seek, +{ + pub entry: crate::storage::Entry, + pub passes: i32, +} + +pub fn execute(stor: Arc + 'static>, req: Request) -> Result<(), Error> +where + RW: Read + Write + Seek, +{ + if !req.entry.is_dir() { + return Err(Error::InvalidFileType); + } + + let files = stor + .read_dir(&req.entry) + .map_err(|_| Error::ReadDirEntries)?; + + #[allow(clippy::needless_collect)] // 🚫 we have to collect in order to propertly join threads! + let handlers = files + .into_iter() + .filter(|f| !f.is_dir()) + .map(|f| { + let file_path = f.path().to_path_buf(); + let stor = stor.clone(); + std::thread::spawn(move || -> Result<(), Error> { + crate::erase::execute( + stor, + crate::erase::Request { + path: file_path, + passes: req.passes, + }, + ) + .map_err(Error::EraseFile)?; + Ok(()) + }) + }) + .collect::>(); + + handlers.into_iter().try_for_each(|h| h.join().unwrap())?; + + stor.remove_dir_all(req.entry).map_err(|_| Error::RemoveDir) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::InMemoryStorage; + + use std::path::PathBuf; + + #[test] + fn should_erase_dir_recursively_with_subfiles() { + let stor = Arc::new(InMemoryStorage::default()); + stor.add_hello_txt(); + stor.add_bar_foo_folder(); + + let file = stor.read_file("bar/").unwrap(); + let file_path = file.path().to_path_buf(); + + let req = Request { + entry: file, + passes: 2, + }; + + match execute(stor.clone(), req) { + Ok(()) => { + assert_eq!(stor.files().get(&file_path).cloned(), None); + let files = stor.files(); + let mut keys = files.keys(); + assert_eq!(keys.next(), Some(&PathBuf::from("hello.txt"))); + assert_eq!(keys.next(), None); + } + _ => unreachable!(), + } + } +} diff --git a/crates/volaris-domain/src/hash.rs b/crates/volaris-domain/src/hash.rs new file mode 100644 index 0000000..35efb7d --- /dev/null +++ b/crates/volaris-domain/src/hash.rs @@ -0,0 +1,126 @@ +//! This provides functionality for hashing a file with `BLAKE3`, using a stream reader to keep memory usage low. + +use core::primitives::BLOCK_SIZE; +use std::fmt; +use std::{ + cell::RefCell, + io::{Read, Seek}, +}; + +use crate::hasher::Hasher; + +#[derive(Debug)] +pub enum Error { + ResetCursorPosition, + ReadData, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::ResetCursorPosition => f.write_str("Unable to reset cursor position"), + Error::ReadData => f.write_str("Unable to read data"), + } + } +} + +impl std::error::Error for Error {} + +pub struct Request { + pub reader: RefCell, +} + +pub fn execute(mut hasher: impl Hasher, req: Request) -> Result { + req.reader + .borrow_mut() + .rewind() + .map_err(|_| Error::ResetCursorPosition)?; + + let mut buffer = vec![0u8; BLOCK_SIZE].into_boxed_slice(); + + loop { + let read_count = req + .reader + .borrow_mut() + .read(&mut buffer) + .map_err(|_| Error::ReadData)?; + hasher.write(&buffer[..read_count]); + if read_count != BLOCK_SIZE { + break; + } + } + + Ok(hasher.finish()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hasher::Blake3Hasher; + use rand::RngCore; + use std::io::Cursor; + + #[test] + fn should_hash_string() { + let text = "Hello world"; + let mut bytes = text.as_bytes(); + let reader = Cursor::new(&mut bytes); + + let req = Request { + reader: RefCell::new(reader), + }; + + match execute(Blake3Hasher::default(), req) { + Err(_) => unreachable!(), + Ok(hash) => { + assert_eq!(hash, blake3::hash(text.as_bytes()).to_hex().to_string()); + } + } + } + + #[test] + fn should_hash_big_string() { + #[allow( + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::cast_precision_loss + )] + let capacity = (BLOCK_SIZE as f32 * 1.5) as usize; + let mut buf = Vec::with_capacity(capacity); + rand::thread_rng().fill_bytes(&mut buf); + + let orig_buf = buf.clone(); + let reader = Cursor::new(&mut buf); + + let req = Request { + reader: RefCell::new(reader), + }; + + match execute(Blake3Hasher::default(), req) { + Err(_) => unreachable!(), + Ok(hash) => { + assert_eq!(hash, blake3::hash(&orig_buf).to_hex().to_string()); + } + } + } + + #[test] + fn should_reset_position_and_make_hash() { + let text = "Hello world"; + let mut bytes = text.as_bytes(); + let mut reader = Cursor::new(&mut bytes); + + reader.seek(std::io::SeekFrom::End(0)).unwrap(); + + let req = Request { + reader: RefCell::new(reader), + }; + + match execute(Blake3Hasher::default(), req) { + Err(_) => unreachable!(), + Ok(hash) => { + assert_eq!(hash, blake3::hash(text.as_bytes()).to_hex().to_string()); + } + } + } +} diff --git a/crates/volaris-domain/src/hasher.rs b/crates/volaris-domain/src/hasher.rs new file mode 100644 index 0000000..06adcf2 --- /dev/null +++ b/crates/volaris-domain/src/hasher.rs @@ -0,0 +1,26 @@ +pub trait Hasher { + fn write(&mut self, input: &[u8]); + fn finish(&mut self) -> String; +} + +pub struct Blake3Hasher { + inner: blake3::Hasher, +} + +impl Default for Blake3Hasher { + fn default() -> Self { + Self { + inner: blake3::Hasher::new(), + } + } +} + +impl Hasher for Blake3Hasher { + fn write(&mut self, input: &[u8]) { + self.inner.update(input); + } + + fn finish(&mut self) -> String { + self.inner.finalize().to_hex().to_string() + } +} diff --git a/crates/volaris-domain/src/header.rs b/crates/volaris-domain/src/header.rs new file mode 100644 index 0000000..bf08243 --- /dev/null +++ b/crates/volaris-domain/src/header.rs @@ -0,0 +1,31 @@ +//! This module contains all volaris header-related functions, such as dumping the header, restoring a dumped header, or stripping it entirely. + +pub mod dump; +pub mod restore; +pub mod strip; + +#[derive(Debug)] +pub enum Error { + UnsupportedRestore, + InvalidFile, + Write, + Read, + HeaderSizeParse, + Rewind, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Error::{HeaderSizeParse, InvalidFile, Read, Rewind, UnsupportedRestore, Write}; + match self { + UnsupportedRestore => f.write_str("The provided request is unsupported with this file. It maybe isn't an encrypted file, or it was encrypted in detached mode."), + InvalidFile => f.write_str("The file does not contain a valid volaris header."), + Write => f.write_str("Unable to write the data."), + Read => f.write_str("Unable to read the data."), + Rewind => f.write_str("Unable to rewind the stream."), + HeaderSizeParse => f.write_str("Unable to parse the size of the header."), + } + } +} + +impl std::error::Error for Error {} diff --git a/crates/volaris-domain/src/header/dump.rs b/crates/volaris-domain/src/header/dump.rs new file mode 100644 index 0000000..e178e83 --- /dev/null +++ b/crates/volaris-domain/src/header/dump.rs @@ -0,0 +1,31 @@ +//! This provides functionality for dumping a header that adheres to the volaris format. + +use super::Error; +use std::cell::RefCell; +use std::io::{Read, Seek, Write}; + +use core::header::Header; + +pub struct Request<'a, R, W> +where + R: Read + Seek, + W: Write + Seek, +{ + pub reader: &'a RefCell, + pub writer: &'a RefCell, +} + +pub fn execute(req: Request<'_, R, W>) -> Result<(), Error> +where + R: Read + Seek, + W: Write + Seek, +{ + let (header, _) = + Header::deserialize(&mut *req.reader.borrow_mut()).map_err(|_| Error::InvalidFile)?; + + header + .write(&mut *req.writer.borrow_mut()) + .map_err(|_| Error::Write)?; + + Ok(()) +} diff --git a/crates/volaris-domain/src/header/restore.rs b/crates/volaris-domain/src/header/restore.rs new file mode 100644 index 0000000..517a298 --- /dev/null +++ b/crates/volaris-domain/src/header/restore.rs @@ -0,0 +1,52 @@ +//! This provides functionality for restoring a dumped header that adheres to the volaris format, provided the target file contains enough empty bytes at the start to do so. + +use super::Error; +use std::cell::RefCell; +use std::io::{Read, Seek, Write}; + +use core::header::Header; + +pub struct Request<'a, R, RW> +where + R: Read + Seek, + RW: Read + Write + Seek, +{ + pub reader: &'a RefCell, + pub writer: &'a RefCell, +} + +pub fn execute(req: Request<'_, R, RW>) -> Result<(), Error> +where + R: Read + Seek, + RW: Read + Write + Seek, +{ + let (header, _) = + Header::deserialize(&mut *req.reader.borrow_mut()).map_err(|_| Error::InvalidFile)?; + + let mut header_bytes = vec![ + 0u8; + header + .get_size() + .try_into() + .map_err(|_| Error::HeaderSizeParse)? + ]; + req.writer + .borrow_mut() + .read(&mut header_bytes) + .map_err(|_| Error::Read)?; + + if !header_bytes.into_iter().all(|b| b == 0) { + return Err(Error::UnsupportedRestore); + } + + req.writer + .borrow_mut() + .rewind() + .map_err(|_| Error::Rewind)?; + + header + .write(&mut *req.writer.borrow_mut()) + .map_err(|_| Error::Write)?; + + Ok(()) +} diff --git a/crates/volaris-domain/src/header/strip.rs b/crates/volaris-domain/src/header/strip.rs new file mode 100644 index 0000000..d4bf5ae --- /dev/null +++ b/crates/volaris-domain/src/header/strip.rs @@ -0,0 +1,42 @@ +//! This provides functionality for stripping a header that adheres to the volaris format. + +use super::Error; +use std::cell::RefCell; +use std::io::{Read, Seek, Write}; + +use core::header::Header; + +pub struct Request<'a, RW> +where + RW: Read + Write + Seek, +{ + pub handle: &'a RefCell, +} + +pub fn execute(req: Request<'_, RW>) -> Result<(), Error> +where + RW: Read + Write + Seek, +{ + let (header, _) = + Header::deserialize(&mut *req.handle.borrow_mut()).map_err(|_| Error::InvalidFile)?; + + req.handle + .borrow_mut() + .rewind() + .map_err(|_| Error::Rewind)?; + + let zeroes = vec![ + 0u8; + header + .get_size() + .try_into() + .map_err(|_| Error::HeaderSizeParse)? + ]; + + req.handle + .borrow_mut() + .write_all(&zeroes) + .map_err(|_| Error::Write)?; + + Ok(()) +} diff --git a/crates/volaris-domain/src/key.rs b/crates/volaris-domain/src/key.rs new file mode 100644 index 0000000..cbe81c2 --- /dev/null +++ b/crates/volaris-domain/src/key.rs @@ -0,0 +1,109 @@ +use core::key::vec_to_arr; +use core::primitives::Algorithm; +use core::primitives::ENCRYPTED_MASTER_KEY_LEN; +use core::primitives::MASTER_KEY_LEN; +use core::protected::Protected; +use core::Zeroize; +use core::{cipher::Ciphers, header::Keyslot}; + +pub mod add; +pub mod change; +pub mod delete; +pub mod verify; + +#[derive(Debug)] +pub enum Error { + HeaderSizeParse, + Unsupported, + IncorrectKey, + MasterKeyEncrypt, + TooManyKeyslots, + KeyHash, + CipherInit, + HeaderDeserialize, + HeaderWrite, + Seek, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::HeaderSizeParse => f.write_str("Cannot parse header size"), + Error::Seek => f.write_str("Unable to seek the data's cursor"), + Error::HeaderWrite => f.write_str("Unable to write the header"), + Error::HeaderDeserialize => f.write_str("Unable to deserialize the header"), + Error::CipherInit => f.write_str("Unable to initialize a cipher"), + Error::KeyHash => f.write_str("Unable to hash your key"), + Error::TooManyKeyslots => { + f.write_str("There are already too many populated keyslots within this file") + } + Error::MasterKeyEncrypt => f.write_str("Unable to encrypt master key"), + Error::Unsupported => { + f.write_str("The provided request is unsupported with this header version") + } + Error::IncorrectKey => f.write_str("The provided key is incorrect"), + } + } +} + +pub fn decrypt_v5_master_key_with_index( + keyslots: &[Keyslot], + raw_key_old: Protected>, + algorithm: &Algorithm, +) -> Result<(Protected<[u8; MASTER_KEY_LEN]>, usize), Error> { + let mut index = 0; + let mut master_key = [0u8; MASTER_KEY_LEN]; + + // we need the index, so we can't use `decrypt_master_key()` + for (i, keyslot) in keyslots.iter().enumerate() { + let key_old = keyslot + .hash_algorithm + .hash(raw_key_old.clone(), &keyslot.salt) + .map_err(|_| Error::KeyHash)?; + let cipher = Ciphers::initialize(key_old, algorithm).map_err(|_| Error::CipherInit)?; + + let master_key_result = cipher.decrypt(&keyslot.nonce, keyslot.encrypted_key.as_slice()); + + if master_key_result.is_err() { + continue; + } + + let mut master_key_decrypted = master_key_result.unwrap(); + let len = MASTER_KEY_LEN.min(master_key_decrypted.len()); + master_key[..len].copy_from_slice(&master_key_decrypted[..len]); + master_key_decrypted.zeroize(); + + index = i; + + drop(cipher); + break; + } + + drop(raw_key_old); + + if master_key == [0u8; MASTER_KEY_LEN] { + return Err(Error::IncorrectKey); + } + + Ok((Protected::new(master_key), index)) +} + +impl std::error::Error for Error {} + +// TODO(brxken128): make this available in the core +pub fn encrypt_master_key( + master_key: Protected<[u8; MASTER_KEY_LEN]>, + key_new: Protected<[u8; 32]>, + nonce: &[u8], + algorithm: &Algorithm, +) -> Result<[u8; ENCRYPTED_MASTER_KEY_LEN], Error> { + let cipher = Ciphers::initialize(key_new, algorithm).map_err(|_| Error::CipherInit)?; + + let master_key_result = cipher.encrypt(nonce, master_key.expose().as_slice()); + + drop(master_key); + + let master_key_encrypted = master_key_result.map_err(|_| Error::MasterKeyEncrypt)?; + + Ok(vec_to_arr(master_key_encrypted)) +} diff --git a/crates/volaris-domain/src/key/add.rs b/crates/volaris-domain/src/key/add.rs new file mode 100644 index 0000000..de38725 --- /dev/null +++ b/crates/volaris-domain/src/key/add.rs @@ -0,0 +1,99 @@ +//! This provides functionality for adding a key to a header that both adheres to the volaris format, and is using a version >= V5. + +use std::io::Seek; + +use super::Error; +use core::header::HashingAlgorithm; +use core::header::Keyslot; +use core::header::{Header, HeaderVersion}; +use core::primitives::gen_nonce; +use core::primitives::gen_salt; +use core::primitives::Mode; +use core::protected::Protected; +use std::cell::RefCell; +use std::io::{Read, Write}; + +pub struct Request<'a, RW> +where + RW: Read + Write + Seek, +{ + pub handle: &'a RefCell, // header read+write+seek + pub raw_key_old: Protected>, + pub raw_key_new: Protected>, + pub hash_algorithm: HashingAlgorithm, +} + +pub fn execute(req: Request<'_, RW>) -> Result<(), Error> +where + RW: Read + Write + Seek, +{ + let (header, _) = core::header::Header::deserialize(&mut *req.handle.borrow_mut()) + .map_err(|_| Error::HeaderDeserialize)?; + + if header.header_type.version < HeaderVersion::V5 { + return Err(Error::Unsupported); + } + + let header_size: i64 = header + .get_size() + .try_into() + .map_err(|_| Error::HeaderSizeParse)?; + + req.handle + .borrow_mut() + .seek(std::io::SeekFrom::Current(-header_size)) + .map_err(|_| Error::Seek)?; + + // this gets modified, then any changes from below are written at the end + let mut keyslots = header.keyslots.clone().unwrap(); + + // all of these functions need either the master key, or the index + let (master_key, _) = super::decrypt_v5_master_key_with_index( + &keyslots, + req.raw_key_old, + &header.header_type.algorithm, + )?; + + if keyslots.len() == 4 { + return Err(Error::TooManyKeyslots); + } + + let salt = gen_salt(); + let master_key_nonce = gen_nonce(&header.header_type.algorithm, &Mode::MemoryMode); + + let key_new = req + .hash_algorithm + .hash(req.raw_key_new, &salt) + .map_err(|_| Error::KeyHash)?; + + let encrypted_master_key = super::encrypt_master_key( + master_key, + key_new, + &master_key_nonce, + &header.header_type.algorithm, + )?; + + let keyslot = Keyslot { + encrypted_key: encrypted_master_key, + nonce: master_key_nonce, + salt, + hash_algorithm: req.hash_algorithm, + }; + + keyslots.push(keyslot); + + // recreate header and inherit everything (except keyslots) + let header_new = Header { + nonce: header.nonce, + salt: header.salt, + keyslots: Some(keyslots), + header_type: header.header_type, + }; + + // write the header to the handle + header_new + .write(&mut *req.handle.borrow_mut()) + .map_err(|_| Error::HeaderWrite)?; + + Ok(()) +} diff --git a/crates/volaris-domain/src/key/change.rs b/crates/volaris-domain/src/key/change.rs new file mode 100644 index 0000000..f7624b9 --- /dev/null +++ b/crates/volaris-domain/src/key/change.rs @@ -0,0 +1,93 @@ +//! This provides functionality for adding a key to a header that both adheres to the volaris format, and is using a version >= V5. + +use std::io::Seek; + +use super::Error; +use core::header::HashingAlgorithm; +use core::header::Keyslot; +use core::header::{Header, HeaderVersion}; +use core::primitives::gen_nonce; +use core::primitives::gen_salt; +use core::primitives::Mode; +use core::protected::Protected; +use std::cell::RefCell; +use std::io::{Read, Write}; + +pub struct Request<'a, RW> +where + RW: Read + Write + Seek, +{ + pub handle: &'a RefCell, // header read+write+seek + pub raw_key_old: Protected>, + pub raw_key_new: Protected>, + pub hash_algorithm: HashingAlgorithm, +} + +pub fn execute(req: Request<'_, RW>) -> Result<(), Error> +where + RW: Read + Write + Seek, +{ + let (header, _) = core::header::Header::deserialize(&mut *req.handle.borrow_mut()) + .map_err(|_| Error::HeaderDeserialize)?; + + if header.header_type.version < HeaderVersion::V5 { + return Err(Error::Unsupported); + } + + let header_size: i64 = header + .get_size() + .try_into() + .map_err(|_| Error::HeaderSizeParse)?; + + req.handle + .borrow_mut() + .seek(std::io::SeekFrom::Current(-header_size)) + .map_err(|_| Error::Seek)?; + + // this gets modified, then any changes from below are written at the end + let mut keyslots = header.keyslots.clone().unwrap(); + + // all of these functions need either the master key, or the index + let (master_key, index) = super::decrypt_v5_master_key_with_index( + &keyslots, + req.raw_key_old, + &header.header_type.algorithm, + )?; + + let salt = gen_salt(); + let key_new = req + .hash_algorithm + .hash(req.raw_key_new, &salt) + .map_err(|_| Error::KeyHash)?; + + let master_key_nonce = gen_nonce(&header.header_type.algorithm, &Mode::MemoryMode); + + let encrypted_master_key = super::encrypt_master_key( + master_key, + key_new, + &master_key_nonce, + &header.header_type.algorithm, + )?; + + keyslots[index] = Keyslot { + encrypted_key: encrypted_master_key, + nonce: master_key_nonce, + salt, + hash_algorithm: req.hash_algorithm, + }; + + // recreate header and inherit everything (except keyslots) + let header_new = Header { + nonce: header.nonce, + salt: header.salt, + keyslots: Some(keyslots), + header_type: header.header_type, + }; + + // write the header to the handle + header_new + .write(&mut *req.handle.borrow_mut()) + .map_err(|_| Error::HeaderWrite)?; + + Ok(()) +} diff --git a/crates/volaris-domain/src/key/delete.rs b/crates/volaris-domain/src/key/delete.rs new file mode 100644 index 0000000..5c7c97e --- /dev/null +++ b/crates/volaris-domain/src/key/delete.rs @@ -0,0 +1,65 @@ +//! This provides functionality for adding a key to a header that both adheres to the volaris format, and is using a version >= V5. + +use super::Error; +use core::header::{Header, HeaderVersion}; +use core::protected::Protected; +use std::cell::RefCell; +use std::io::Seek; +use std::io::{Read, Write}; + +pub struct Request<'a, RW> +where + RW: Read + Write + Seek, +{ + pub handle: &'a RefCell, // header read+write+seek + pub raw_key_old: Protected>, +} + +pub fn execute(req: Request<'_, RW>) -> Result<(), Error> +where + RW: Read + Write + Seek, +{ + let (header, _) = core::header::Header::deserialize(&mut *req.handle.borrow_mut()) + .map_err(|_| Error::HeaderDeserialize)?; + + if header.header_type.version < HeaderVersion::V5 { + return Err(Error::Unsupported); + } + + let header_size: i64 = header + .get_size() + .try_into() + .map_err(|_| Error::HeaderSizeParse)?; + + req.handle + .borrow_mut() + .seek(std::io::SeekFrom::Current(-header_size)) + .map_err(|_| Error::Seek)?; + + // this gets modified, then any changes from below are written at the end + let mut keyslots = header.keyslots.clone().unwrap(); + + // all of these functions need either the master key, or the index + let (_, index) = super::decrypt_v5_master_key_with_index( + &keyslots, + req.raw_key_old, + &header.header_type.algorithm, + )?; + + keyslots.remove(index); + + // recreate header and inherit everything (except keyslots) + let header_new = Header { + nonce: header.nonce, + salt: header.salt, + keyslots: Some(keyslots), + header_type: header.header_type, + }; + + // write the header to the handle + header_new + .write(&mut *req.handle.borrow_mut()) + .map_err(|_| Error::HeaderWrite)?; + + Ok(()) +} diff --git a/crates/volaris-domain/src/key/verify.rs b/crates/volaris-domain/src/key/verify.rs new file mode 100644 index 0000000..f268f02 --- /dev/null +++ b/crates/volaris-domain/src/key/verify.rs @@ -0,0 +1,43 @@ +//! This provides functionality for verifying that a decryption key is correct (header version >= V5) + +use std::io::Seek; + +use super::Error; +use core::header::HeaderVersion; +use core::protected::Protected; +use std::cell::RefCell; +use std::io::Read; + +pub struct Request<'a, R> +where + R: Read + Seek, +{ + pub handle: &'a RefCell, // header read+write+seek + pub raw_key: Protected>, +} + +pub fn execute(req: Request<'_, R>) -> Result<(), Error> +where + R: Read + Seek, +{ + let (header, _) = core::header::Header::deserialize(&mut *req.handle.borrow_mut()) + .map_err(|_| Error::HeaderDeserialize)?; + + if header.header_type.version < HeaderVersion::V5 { + return Err(Error::Unsupported); + } + + let keyslots = header.keyslots.clone().unwrap(); + + // all of these functions need either the master key, or the index + let (master_key, _) = super::decrypt_v5_master_key_with_index( + &keyslots, + req.raw_key, + &header.header_type.algorithm, + )?; + + // ensure the master key is gone from memory in the event that the key is correct + drop(master_key); + + Ok(()) +} diff --git a/crates/volaris-domain/src/lib.rs b/crates/volaris-domain/src/lib.rs new file mode 100644 index 0000000..0c10760 --- /dev/null +++ b/crates/volaris-domain/src/lib.rs @@ -0,0 +1,67 @@ +//! ## What is it? +//! +//! volaris-Domain is a library used for managing the core logic behind volaris, and any applications that require easy integration with the volaris format. +//! +//! ## Security +//! +//! volaris-Domain is built on top of volaris-Core - which uses modern, secure and audited1 AEADs for encryption and decryption. +//! +//! You may find the audits for both AES-256-GCM and XChaCha20-Poly1305 on [the NCC Group's website](https://research.nccgroup.com/2020/02/26/public-report-rustcrypto-aes-gcm-and-chacha20poly1305-implementation-review/). +//! +//! 1 Deoxys-II-256 does not have an official audit, so use it at your own risk +//! +//! ## Who uses volaris-Domain? +//! +//! This library is implemented by [volaris](https://github.com/brxken128/volaris), a secure command-line file +//! encryption utility. +//! +//! This crate was made to separate the logic away from the end-user application. +//! +//! It also allows for more things to be built on top of the core functionality, such as a GUI application. +//! +//! ## Donating +//! +//! If you like my work, and want to help support volaris, volaris-Core or volaris-Domain, feel free to donate! This is not necessary by any means, so please don't feel obliged to do so. +//! +//! ```text +//! XMR: 84zSGS18aHtT3CZjZUnnWpCsz1wmA5f65G6BXisbrvAiH7PxZpP8GorbdjAQYRtfeiANZywwUPjZcHu8eXJeWdafJQFK46G +//! BTC: bc1q8x0r7khrfj40qd0zr5xv3t9nl92rz2387pu48u +//! ETH: 0x9630f95F11dFa8703b71DbF746E5c83A31A3F2DD +//! ``` +//! +//! You can read more about volaris, volaris-Core, volaris-Domain and the technical details [in the project's main documentation](https://brxken128.github.io/volaris/)! +//! + +// lints +#![forbid(unsafe_code)] +#![warn( + rust_2018_idioms, + non_ascii_idents, + unstable_features, + unused_imports, + unused_qualifications, + clippy::pedantic, + clippy::all +)] +#![allow( + clippy::module_name_repetitions, + clippy::similar_names, + clippy::needless_pass_by_value, + clippy::missing_panics_doc, + clippy::missing_errors_doc +)] + +pub mod decrypt; +pub mod encrypt; +pub mod erase; +pub mod erase_dir; +pub mod hash; +pub mod hasher; +pub mod header; +pub mod key; +pub mod overwrite; +pub mod pack; +pub mod storage; +pub mod unpack; + +pub mod utils; diff --git a/crates/volaris-domain/src/overwrite.rs b/crates/volaris-domain/src/overwrite.rs new file mode 100644 index 0000000..3bdf5e9 --- /dev/null +++ b/crates/volaris-domain/src/overwrite.rs @@ -0,0 +1,125 @@ +//! This contains the actual logic for "shredding" a file. +//! +//! This will not be effective on flash storage, and if you are planning to release a program that uses this function, I'd recommend putting the default number of passes to 1. + +use rand::RngCore; +use std::cell::RefCell; +use std::fmt; +use std::io::{Seek, Write}; + +const BLOCK_SIZE: usize = 512; + +#[derive(Debug)] +pub enum Error { + ResetCursorPosition, + OverwriteWithRandomBytes, + OverwriteWithZeros, + FlushFile, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::ResetCursorPosition => f.write_str("Unable to reset cursor position"), + Error::OverwriteWithRandomBytes => f.write_str("Unable to overwrite with random bytes"), + Error::OverwriteWithZeros => f.write_str("Unable to overwrite with zeros"), + Error::FlushFile => f.write_str("Unable to flush"), + } + } +} + +impl std::error::Error for Error {} + +pub struct Request<'a, W: Write + Seek> { + pub writer: &'a RefCell, + pub buf_capacity: usize, + pub passes: i32, +} + +pub fn execute(req: Request<'_, W>) -> Result<(), Error> { + let mut writer = req.writer.borrow_mut(); + for _ in 0..req.passes { + writer.rewind().map_err(|_| Error::ResetCursorPosition)?; + + let mut blocks = vec![BLOCK_SIZE].repeat(req.buf_capacity / BLOCK_SIZE); + blocks.push(req.buf_capacity % BLOCK_SIZE); + + for block_size in blocks.into_iter().take_while(|bs| *bs > 0) { + let mut block_buf = Vec::with_capacity(block_size); + rand::thread_rng().fill_bytes(&mut block_buf); + writer + .write_all(&block_buf) + .map_err(|_| Error::OverwriteWithRandomBytes)?; + } + + writer.flush().map_err(|_| Error::FlushFile)?; + } + + writer.rewind().map_err(|_| Error::ResetCursorPosition)?; + writer + .write_all(&[0].repeat(req.buf_capacity)) + .map_err(|_| Error::OverwriteWithZeros)?; + writer.flush().map_err(|_| Error::FlushFile) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + fn make_test(capacity: usize, passes: i32) { + let mut buf = Vec::with_capacity(capacity); + rand::thread_rng().fill_bytes(&mut buf); + + let writer = Cursor::new(&mut buf); + + let req = Request { + writer: &RefCell::new(writer), + buf_capacity: capacity, + passes, + }; + + match execute(req) { + Ok(_) => { + assert_eq!(buf.len(), capacity); + assert_eq!(buf, vec![0].repeat(capacity)); + } + _ => unreachable!(), + } + } + + #[test] + fn should_overwrite_empty_content() { + make_test(0, 1); + } + + #[test] + fn should_overwrite_small_content() { + make_test(100, 1); + } + + #[test] + fn should_overwrite_perfectly_divisible_content() { + make_test(BLOCK_SIZE, 1); + } + + #[test] + fn should_overwrite_not_perfectly_divisible_content() { + make_test(515, 1); + } + + #[test] + fn should_overwrite_large_content() { + make_test(BLOCK_SIZE * 100, 1); + } + + #[test] + fn should_erase_fill_random_bytes_one_hundred_times() { + make_test(515, 100); + } + + #[test] + fn should_erase_fill_random_bytes_zero_times() { + make_test(515, 0); + } +} diff --git a/crates/volaris-domain/src/pack.rs b/crates/volaris-domain/src/pack.rs new file mode 100644 index 0000000..7619b17 --- /dev/null +++ b/crates/volaris-domain/src/pack.rs @@ -0,0 +1,244 @@ +//! This contains the logic for traversing a given directory, placing all of the files within a zip file, and encrypting the zip file. The temporary zip file is then erased with one pass. +//! +//! This is known as "packing" within volaris. +//! +//! DISCLAIMER: Encryption with compression is generally not recommended, however here it is fine. As the data is at-rest, and it's assumed you have complete control over the data you're encrypting (e.g. not attacker-controlled), there should be no problems. Feel free to use no compression if you feel otherwise. + +use std::cell::RefCell; +use std::io::{BufWriter, Read, Seek, Write}; +use std::sync::Arc; + +use core::header::{HashingAlgorithm, HeaderType}; +use core::primitives::BLOCK_SIZE; +use core::protected::Protected; +use zip::write::FileOptions; + +use crate::storage::Storage; + +#[derive(Debug)] +pub enum Error { + CreateArchive, + AddDirToArchive, + AddFileToArchive, + FinishArchive, + ReadData, + WriteData, + Encrypt(crate::encrypt::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CreateArchive => f.write_str("Unable to create archive"), + Error::AddDirToArchive => f.write_str("Unable to add directory to archive"), + Error::AddFileToArchive => f.write_str("Unable to add file to archive"), + Error::FinishArchive => f.write_str("Unable to finish archive"), + Error::ReadData => f.write_str("Unable to read data"), + Error::WriteData => f.write_str("Unable to write data"), + Error::Encrypt(inner) => write!(f, "Unable to encrypt archive: {inner}"), + } + } +} + +impl std::error::Error for Error {} + +pub struct Request<'a, RW> +where + RW: Read + Write + Seek, +{ + pub writer: &'a RefCell, + pub compress_files: Vec>, + pub compression_method: zip::CompressionMethod, + pub header_writer: Option<&'a RefCell>, + pub raw_key: Protected>, + // TODO: don't use external types in logic + pub header_type: HeaderType, + pub hashing_algorithm: HashingAlgorithm, +} + +pub fn execute(stor: Arc>, req: Request<'_, RW>) -> Result<(), Error> +where + RW: Read + Write + Seek, +{ + // 1. Create zip archive. + let tmp_file = stor.create_temp_file().map_err(|_| Error::CreateArchive)?; + { + let mut tmp_writer = tmp_file + .try_writer() + .map_err(|_| Error::CreateArchive)? + .borrow_mut(); + let mut zip_writer = zip::ZipWriter::new(BufWriter::new(&mut *tmp_writer)); + + let options = FileOptions::default() + .compression_method(req.compression_method) + .large_file(true) + .unix_permissions(0o755); + + // 2. Add files to the archive. + req.compress_files.into_iter().try_for_each(|f| { + let file_path = f.path().to_str().ok_or(Error::ReadData)?; + if f.is_dir() { + zip_writer + .add_directory(file_path, options) + .map_err(|_| Error::AddDirToArchive)?; + } else { + zip_writer + .start_file(file_path, options) + .map_err(|_| Error::AddFileToArchive)?; + + let mut reader = f.try_reader().map_err(|_| Error::ReadData)?.borrow_mut(); + let mut buffer = vec![0u8; BLOCK_SIZE].into_boxed_slice(); + loop { + let read_count = reader.read(&mut buffer).map_err(|_| Error::ReadData)?; + zip_writer + .write_all(&buffer[..read_count]) + .map_err(|_| Error::WriteData)?; + if read_count != BLOCK_SIZE { + break; + } + } + } + + Ok(()) + })?; + + // 3. Close archive and switch writer to reader. + zip_writer.finish().map_err(|_| Error::FinishArchive)?; + } + + let buf_capacity = stor.file_len(&tmp_file).map_err(|_| Error::FinishArchive)?; + + // 4. Encrypt zip archive + let encrypt_res = crate::encrypt::execute(crate::encrypt::Request { + reader: tmp_file.try_reader().map_err(|_| Error::FinishArchive)?, + writer: req.writer, + header_writer: req.header_writer, + raw_key: req.raw_key, + header_type: req.header_type, + hashing_algorithm: req.hashing_algorithm, + }) + .map_err(Error::Encrypt); + + // 5. Finally eraze zip archive with zeros. + crate::overwrite::execute(crate::overwrite::Request { + buf_capacity, + writer: tmp_file.try_writer().map_err(|_| Error::FinishArchive)?, + passes: 2, + }) + .ok(); + + stor.remove_file(tmp_file).ok(); + + encrypt_res +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Read; + + use core::header::{HeaderType, HeaderVersion}; + use core::primitives::{Algorithm, Mode}; + + use crate::encrypt::tests::PASSWORD; + use crate::storage::{InMemoryStorage, Storage}; + + const ENCRYPTED_PACKED_BAR_DIR: [u8; 1202] = [ + 222, 5, 14, 1, 12, 1, 173, 240, 60, 45, 230, 243, 58, 160, 69, 50, 217, 192, 66, 223, 124, + 190, 148, 91, 92, 129, 0, 0, 0, 0, 0, 0, 223, 181, 71, 240, 140, 106, 41, 36, 82, 150, 105, + 215, 159, 108, 234, 246, 25, 19, 65, 206, 177, 146, 15, 174, 209, 129, 82, 2, 62, 76, 129, + 34, 136, 189, 11, 98, 105, 54, 146, 71, 102, 166, 97, 177, 207, 62, 194, 132, 38, 87, 173, + 240, 60, 45, 230, 243, 58, 160, 69, 50, 217, 192, 66, 223, 124, 190, 148, 91, 92, 129, 50, + 126, 110, 254, 58, 206, 16, 183, 233, 128, 23, 223, 81, 30, 214, 132, 32, 104, 51, 119, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 64, 6, 177, 49, + 139, 218, 8, 121, 228, 19, 5, 8, 117, 33, 131, 131, 70, 76, 147, 108, 49, 191, 191, 127, + 223, 64, 127, 248, 65, 201, 130, 166, 129, 195, 245, 241, 188, 143, 148, 191, 86, 7, 102, + 124, 253, 12, 44, 172, 79, 236, 207, 68, 229, 117, 49, 250, 55, 6, 48, 86, 48, 244, 189, + 137, 27, 142, 241, 44, 118, 35, 5, 138, 237, 47, 248, 108, 30, 224, 42, 91, 16, 216, 14, + 235, 132, 33, 123, 83, 188, 196, 205, 18, 71, 152, 231, 231, 127, 182, 29, 156, 157, 203, + 178, 178, 3, 216, 51, 84, 28, 67, 91, 255, 14, 124, 180, 131, 80, 48, 27, 111, 195, 39, + 127, 37, 231, 111, 82, 132, 168, 253, 149, 230, 199, 161, 78, 6, 175, 98, 210, 9, 25, 145, + 199, 151, 38, 142, 199, 217, 35, 247, 168, 73, 138, 94, 175, 45, 0, 184, 252, 55, 250, 19, + 8, 79, 247, 38, 230, 133, 143, 66, 27, 69, 96, 183, 201, 238, 81, 114, 131, 123, 229, 78, + 39, 140, 151, 4, 196, 49, 37, 58, 12, 48, 243, 83, 111, 84, 6, 82, 249, 200, 120, 238, 190, + 136, 135, 189, 34, 237, 52, 18, 23, 43, 164, 113, 31, 111, 221, 119, 216, 110, 0, 74, 53, + 81, 86, 83, 234, 70, 69, 194, 224, 96, 26, 47, 133, 49, 147, 204, 96, 125, 165, 105, 182, + 161, 2, 143, 225, 195, 95, 64, 24, 49, 236, 210, 124, 32, 214, 69, 201, 5, 73, 5, 7, 160, + 233, 35, 202, 226, 40, 104, 45, 214, 0, 39, 55, 167, 203, 184, 145, 150, 233, 119, 115, + 246, 55, 162, 5, 154, 147, 144, 69, 217, 185, 39, 82, 223, 87, 132, 164, 148, 85, 234, 15, + 160, 2, 214, 133, 27, 73, 53, 27, 86, 53, 215, 96, 142, 85, 25, 127, 11, 111, 19, 1, 72, + 74, 92, 16, 14, 98, 20, 203, 163, 227, 160, 192, 158, 223, 99, 116, 212, 137, 101, 150, + 182, 125, 244, 59, 20, 157, 129, 149, 34, 21, 136, 185, 41, 242, 168, 45, 135, 100, 219, + 239, 132, 211, 238, 37, 242, 139, 218, 120, 112, 158, 75, 53, 172, 162, 136, 202, 94, 117, + 152, 175, 205, 34, 198, 99, 49, 174, 187, 80, 151, 225, 169, 120, 192, 77, 61, 38, 2, 158, + 45, 216, 78, 215, 134, 255, 7, 46, 144, 119, 60, 168, 202, 24, 239, 147, 122, 58, 48, 50, + 178, 58, 153, 243, 242, 169, 238, 42, 78, 123, 37, 181, 17, 109, 175, 84, 6, 212, 122, 89, + 60, 111, 248, 41, 156, 214, 222, 151, 212, 52, 10, 221, 69, 1, 215, 170, 76, 149, 134, 241, + 212, 217, 131, 179, 34, 240, 124, 224, 192, 105, 34, 254, 165, 211, 100, 169, 240, 171, + 131, 50, 80, 54, 254, 128, 179, 233, 223, 22, 39, 56, 205, 221, 76, 177, 197, 164, 140, + 181, 42, 154, 82, 239, 240, 127, 211, 45, 146, 57, 154, 151, 153, 112, 215, 222, 199, 37, + 44, 98, 118, 182, 189, 15, 139, 88, 227, 37, 149, 107, 13, 123, 201, 51, 61, 67, 220, 161, + 13, 72, 176, 39, 157, 128, 105, 144, 10, 46, 29, 113, 1, 76, 162, 157, 200, 213, 175, 107, + 128, 13, 47, 170, 216, 107, 48, 241, 149, 219, 20, 186, 74, 210, 5, 210, 18, 201, 78, 159, + 121, 180, 195, 154, 176, 154, 255, 21, 5, 86, 212, 181, 237, 131, 116, 59, 241, 57, 24, + 102, 126, 132, 135, 154, 99, 217, 2, 201, 139, 202, 125, 64, 165, 195, 210, 255, 165, 197, + 172, 166, 27, 200, 226, 158, 225, 224, 10, 150, 97, 2, 77, 73, 51, 112, 201, 146, 74, 245, + 95, 191, 244, 128, 170, 109, 227, 44, 24, 11, 216, 35, 137, 61, 120, 207, 212, 57, 229, 70, + 152, 118, 92, 235, 187, 55, 189, 231, 126, 15, 86, 66, 78, 251, 39, 181, 191, 193, 226, + 199, 131, 61, 145, 177, 76, 168, 0, 235, 172, 21, 213, 87, 81, 176, 135, 139, 61, 3, 91, + 67, 84, 199, 40, 113, 140, 68, 174, 34, 199, 50, 33, 187, 208, 209, 155, 237, 140, 16, 204, + 135, 151, 241, 28, 95, 87, 91, 169, 160, 1, 206, 18, 220, 65, 236, 52, 63, 184, 226, 237, + 129, 19, 170, 194, 11, 154, 168, 110, 242, 19, 167, 195, 205, 68, 4, 151, 99, 196, 164, 13, + 137, 140, 175, 134, 102, 47, 63, 0, 229, 73, 218, 226, 121, 230, 98, 31, 102, 161, 40, 233, + 229, 39, 224, 19, 92, 220, 151, 154, 193, 191, 30, + ]; + + #[test] + fn should_pack_bar_directory() { + let stor = Arc::new(InMemoryStorage::default()); + stor.add_hello_txt(); + stor.add_bar_foo_folder_with_hidden(); + + let file = stor.read_file("bar/").unwrap(); + let mut compress_files = stor.read_dir(&file).unwrap(); + compress_files.sort_by(|a, b| a.path().cmp(b.path())); + + let output_file = stor.create_file("bar.zip.enc").unwrap(); + + let req = Request { + compress_files, + compression_method: zip::CompressionMethod::Stored, + writer: output_file.try_writer().unwrap(), + header_writer: None, + raw_key: Protected::new(PASSWORD.to_vec()), + header_type: HeaderType { + version: HeaderVersion::V5, + algorithm: Algorithm::XChaCha20Poly1305, + mode: Mode::StreamMode, + }, + hashing_algorithm: HashingAlgorithm::Blake3Balloon(5), + }; + + match execute(stor, req) { + Ok(()) => { + let reader = &mut *output_file.try_writer().unwrap().borrow_mut(); + reader.rewind().unwrap(); + + let mut content = vec![]; + reader.read_to_end(&mut content).unwrap(); + + assert_eq!(content, ENCRYPTED_PACKED_BAR_DIR.to_vec()); + } + _ => unreachable!(), + } + } +} diff --git a/crates/volaris-domain/src/storage.rs b/crates/volaris-domain/src/storage.rs new file mode 100644 index 0000000..5a71a5f --- /dev/null +++ b/crates/volaris-domain/src/storage.rs @@ -0,0 +1,743 @@ +use rand::distributions::{Alphanumeric, DistString}; +use std::cell::RefCell; +use std::fs; +use std::io::{Read, Seek, Write}; +use std::path::{Path, PathBuf}; + +#[cfg(test)] +use std::collections::HashMap; +#[cfg(test)] +use std::io; +#[cfg(test)] +use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +#[cfg(test)] +use std::thread; + +#[derive(Debug)] +pub enum FileMode { + Read, + Write, +} + +#[derive(Debug)] +pub enum Error { + CreateDir, + CreateFile, + OpenFile(FileMode), + RemoveFile, + RemoveDir, + DirEntries, + FlushFile, + FileAccess, + FileLen, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CreateDir => f.write_str("Unable to create a new directory"), + Error::CreateFile => f.write_str("Unable to create a new file"), + Error::OpenFile(mode) => write!(f, "Unable to read the file in {mode:?} mode"), + Error::FlushFile => f.write_str("Unable to flush the file"), + Error::RemoveFile => f.write_str("Unable to remove the file"), + Error::RemoveDir => f.write_str("Unable to remove dir"), + Error::DirEntries => f.write_str("Unable to read directory"), + Error::FileAccess => f.write_str("Permission denied"), + Error::FileLen => f.write_str("Unable to get file length"), + } + } +} + +impl std::error::Error for Error {} + +pub trait Storage: Send + Sync +where + RW: Read + Write + Seek, +{ + // TODO(pleshevskiy): return a new struct that will be removed on drop. + fn create_temp_file(&self) -> Result, Error> { + let mut path = std::env::temp_dir(); + let file_name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); + path.push(file_name); + + self.create_file(path) + } + + fn create_dir_all>(&self, path: P) -> Result<(), Error>; + fn create_file>(&self, path: P) -> Result, Error>; + fn read_file>(&self, path: P) -> Result, Error>; + fn write_file>(&self, path: P) -> Result, Error>; + fn flush_file(&self, file: &Entry) -> Result<(), Error>; + fn file_len(&self, file: &Entry) -> Result; + fn remove_file(&self, file: Entry) -> Result<(), Error>; + fn remove_dir_all(&self, file: Entry) -> Result<(), Error>; + // TODO(pleshevskiy): return iterator instead of Vector + fn read_dir(&self, file: &Entry) -> Result>, Error>; +} + +pub struct FileStorage; + +impl Storage for FileStorage { + fn create_dir_all>(&self, path: P) -> Result<(), Error> { + fs::create_dir_all(&path).map_err(|_| Error::CreateDir) + } + + fn create_file>(&self, path: P) -> Result, Error> { + let path = path.as_ref().to_path_buf(); + let file = fs::File::options() + .create_new(true) + .read(true) + .write(true) + .open(&path) + .map_err(|_| Error::CreateFile)?; + Ok(Entry::File(FileData { + path, + stream: RefCell::new(file), + })) + } + + fn read_file>(&self, path: P) -> Result, Error> { + let path = path.as_ref().to_path_buf(); + if path.is_dir() { + Ok(Entry::Dir(path)) + } else { + let file = fs::File::open(&path).map_err(|_| Error::OpenFile(FileMode::Read))?; + Ok(Entry::File(FileData { + path, + stream: RefCell::new(file), + })) + } + } + + fn write_file>(&self, path: P) -> Result, Error> { + let path = path.as_ref().to_path_buf(); + let file = fs::File::options() + .write(true) + .read(true) + .truncate(true) + .open(&path) + .map_err(|_| Error::OpenFile(FileMode::Write))?; + + Ok(Entry::File(FileData { + path, + stream: RefCell::new(file), + })) + } + + fn flush_file(&self, file: &Entry) -> Result<(), Error> { + file.try_writer()? + .borrow_mut() + .flush() + .map_err(|_| Error::FlushFile) + } + + fn file_len(&self, file: &Entry) -> Result { + let fs_file = match file { + Entry::File(FileData { stream, .. }) => stream.borrow(), + Entry::Dir(_) => return Err(Error::FileAccess), + }; + let file_meta = fs::File::metadata(&fs_file).map_err(|_| Error::FileLen)?; + file_meta.len().try_into().map_err(|_| Error::FileLen) + } + + fn remove_file(&self, file: Entry) -> Result<(), Error> { + if let Entry::File(FileData { stream, .. }) = &file { + let mut stream = stream.borrow_mut(); + stream.set_len(0).map_err(|_| Error::RemoveFile)?; + stream.flush().map_err(|_| Error::FlushFile)?; + } + + fs::remove_file(file.path()).map_err(|_| Error::RemoveFile) + } + + fn remove_dir_all(&self, file: Entry) -> Result<(), Error> { + if !file.is_dir() { + return Err(Error::RemoveDir); + } + + fs::remove_dir_all(file.path()).map_err(|_| Error::RemoveDir) + } + + fn read_dir(&self, file: &Entry) -> Result>, Error> { + if !file.is_dir() { + return Err(Error::FileAccess); + } + + walkdir::WalkDir::new(file.path()) + .into_iter() + .map(|res| { + res.map(|e| e.path().to_owned()) + .map_err(|_| Error::DirEntries) + }) + .map(|path| path.and_then(|path| self.read_file(path))) + .collect() + } +} + +#[cfg(test)] +#[derive(Default)] +pub struct InMemoryStorage { + pub files: RwLock>, +} + +#[cfg(test)] +impl InMemoryStorage { + fn save_text_file>(&self, path: P, content: &str) { + let buf = content.bytes().collect::>(); + self.save_file( + path, + IMFile::File(InMemoryFile { + len: buf.len(), + buf, + }), + ); + } + + fn save_file>(&self, path: P, im_file: IMFile) { + self.mut_files().insert(path.as_ref().to_owned(), im_file); + } + + pub(crate) fn files(&self) -> RwLockReadGuard<'_, HashMap> { + loop { + match self.files.try_read() { + Ok(files) => break files, + _ => thread::sleep(std::time::Duration::from_micros(100)), + } + } + } + + pub(crate) fn mut_files(&self) -> RwLockWriteGuard<'_, HashMap> { + loop { + match self.files.try_write() { + Ok(files) => break files, + _ => thread::sleep(std::time::Duration::from_micros(100)), + } + } + } + + // -------------------------------- + // TEST DATA + // ------------------------------- + + pub(crate) fn add_hello_txt(&self) { + self.save_text_file("hello.txt", "hello world"); + } + + pub(crate) fn add_bar_foo_folder(&self) { + self.save_file("bar/", IMFile::Dir); + self.save_text_file("bar/hello.txt", "hello"); + self.save_text_file("bar/world.txt", "world"); + self.save_file("bar/foo/", IMFile::Dir); + self.save_text_file("bar/foo/hello.txt", "hello"); + self.save_text_file("bar/foo/world.txt", "world"); + } + + pub(crate) fn add_bar_foo_folder_with_hidden(&self) { + self.save_file("bar/", IMFile::Dir); + self.save_text_file("bar/.hello.txt", "hello"); + self.save_text_file("bar/world.txt", "world"); + self.save_file("bar/.foo/", IMFile::Dir); + self.save_text_file("bar/.foo/hello.txt", "hello"); + self.save_text_file("bar/.foo/world.txt", "world"); + } +} + +#[cfg(test)] +impl Storage>> for InMemoryStorage { + fn create_dir_all>(&self, _path: P) -> Result<(), Error> { + todo!(); + } + + fn create_file>(&self, path: P) -> Result>>, Error> { + let file_path = path.as_ref().to_path_buf(); + + #[allow(clippy::significant_drop_in_scrutinee)] + let im_file = match self.files().get(&file_path) { + Some(_) => Err(Error::CreateFile), + None => Ok(IMFile::File(InMemoryFile::default())), + }?; + + let cursor = io::Cursor::new(im_file.inner().buf.clone()); + + self.save_file(file_path.clone(), im_file); + + Ok(Entry::File(FileData { + path: file_path, + stream: RefCell::new(cursor), + })) + } + + fn read_file>(&self, path: P) -> Result>>, Error> { + let in_file = self + .files() + .get(path.as_ref()) + .cloned() + .ok_or(Error::OpenFile(FileMode::Read))?; + + let file_path = path.as_ref().to_path_buf(); + + match in_file { + IMFile::Dir => Ok(Entry::Dir(file_path)), + IMFile::File(f) => { + let cursor = io::Cursor::new(f.buf); + Ok(Entry::File(FileData { + path: file_path, + stream: RefCell::new(cursor), + })) + } + } + } + + fn write_file>(&self, path: P) -> Result>>, Error> { + let file_path = path.as_ref().to_path_buf(); + + let file = self + .files() + .get(&file_path) + .cloned() + .ok_or(Error::OpenFile(FileMode::Write))?; + if matches!(file, IMFile::Dir) { + return Err(Error::FileAccess); + } + + let cursor = io::Cursor::new(file.inner().buf.clone()); + + Ok(Entry::File(FileData { + path: file_path, + stream: RefCell::new(cursor), + })) + } + + fn flush_file(&self, file: &Entry>>) -> Result<(), Error> { + if file.is_dir() { + return Err(Error::FileAccess); + } + + let file_path = file.path(); + let writer = file.try_writer()?; + writer.borrow_mut().flush().map_err(|_| Error::FlushFile)?; + + let vec = writer.borrow().get_ref().clone(); + let len = vec.len(); + let new_file = IMFile::File(InMemoryFile { buf: vec, len }); + + self.save_file(file_path, new_file); + + Ok(()) + } + + fn file_len(&self, file: &Entry>>) -> Result { + let cur = match file { + Entry::File(FileData { stream, .. }) => stream.borrow(), + Entry::Dir(_) => return Err(Error::FileAccess), + }; + + Ok(cur.get_ref().len()) + } + + fn remove_file(&self, file: Entry>>) -> Result<(), Error> { + self.mut_files() + .remove(file.path()) + .ok_or(Error::RemoveFile)?; + Ok(()) + } + + fn remove_dir_all(&self, file: Entry>>) -> Result<(), Error> { + if !file.is_dir() { + return Err(Error::FileAccess); + } + + let file_path = file.path(); + + #[allow(clippy::needless_collect)] // 🚫 we have to collect to close read lock guard! + let file_paths = self + .files() + .keys() + .filter(|k| k.starts_with(file_path)) + .cloned() + .collect::>(); + + file_paths.into_iter().try_for_each(|k| { + self.mut_files() + .remove(&k) + .map(|_| ()) + .ok_or(Error::RemoveDir)?; + Ok(()) + }) + } + + fn read_dir( + &self, + file: &Entry>>, + ) -> Result>>>, Error> { + if !file.is_dir() { + return Err(Error::FileAccess); + } + + let file_path = file.path(); + + self.files() + .iter() + .filter(|(k, _)| k.starts_with(file_path)) + .map(|(k, _)| self.read_file(k)) + .collect() + } +} + +#[cfg(test)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct InMemoryFile { + pub buf: Vec, + pub len: usize, +} + +#[cfg(test)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IMFile { + File(InMemoryFile), + Dir, +} + +#[cfg(test)] +impl IMFile { + fn inner(&self) -> &InMemoryFile { + match self { + IMFile::File(inner) => inner, + IMFile::Dir => unreachable!(), + } + } +} + +pub struct FileData +where + RW: Read + Write + Seek, +{ + path: PathBuf, + stream: RefCell, +} + +pub enum Entry +where + RW: Read + Write + Seek, +{ + File(FileData), + Dir(PathBuf), +} + +impl Entry +where + RW: Read + Write + Seek, +{ + pub fn path(&self) -> &Path { + match self { + Entry::File(FileData { path, .. }) | Entry::Dir(path) => path, + } + } + + pub fn is_dir(&self) -> bool { + matches!(self, Entry::Dir(_)) + } + + pub fn try_reader(&self) -> Result<&RefCell, Error> { + match self { + Entry::File(file) => Ok(&file.stream), + Entry::Dir(_) => Err(Error::FileAccess), + } + } + + pub fn try_writer(&self) -> Result<&RefCell, Error> { + match self { + Entry::File(file) => Ok(&file.stream), + Entry::Dir(_) => Err(Error::FileAccess), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn sorted_file_names(file_names: &[PathBuf]) -> Vec<&str> { + let mut keys = file_names + .iter() + .map(|k| k.to_str().unwrap()) + .collect::>(); + keys.sort_unstable(); + keys + } + + #[test] + fn should_create_a_new_file() { + let stor = InMemoryStorage::default(); + + match stor.create_file("hello.txt") { + Ok(file) => { + let im_file = stor.files().get(file.path()).cloned(); + assert_eq!(im_file, Some(IMFile::File(InMemoryFile::default()))); + } + _ => unreachable!(), + } + } + + #[test] + fn should_throw_an_error_if_file_already_exist() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + + match stor.create_file("hello.txt") { + Err(Error::CreateFile) => {} + _ => unreachable!(), + } + } + + #[test] + fn should_not_open_file_to_read() { + let stor = InMemoryStorage::default(); + + match stor.read_file("hello.txt") { + Err(Error::OpenFile(FileMode::Read)) => {} + _ => unreachable!(), + } + } + + #[test] + fn should_not_open_file_to_write() { + let stor = InMemoryStorage::default(); + + match stor.write_file("hello.txt") { + Err(Error::OpenFile(FileMode::Write)) => {} + _ => unreachable!(), + } + } + + #[test] + fn should_open_exist_file_in_read_mode() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + + match stor.read_file("hello.txt") { + Ok(file) => { + if let Some(IMFile::File(InMemoryFile { buf, len })) = stor.files().get(file.path()) + { + let content = b"hello world".to_vec(); + assert_eq!(len, &content.len()); + assert_eq!(buf, &content); + } else { + unreachable!(); + } + } + _ => unreachable!(), + } + } + + #[test] + fn should_open_exist_file_in_write_mode() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + + match stor.write_file("hello.txt") { + Ok(file) => { + if let Some(IMFile::File(InMemoryFile { buf, len })) = stor.files().get(file.path()) + { + let content = b"hello world".to_vec(); + assert_eq!(len, &content.len()); + assert_eq!(buf, &content); + } else { + unreachable!(); + } + } + _ => unreachable!(), + } + } + + #[test] + fn should_write_content_to_file() { + let stor = InMemoryStorage::default(); + let content = "hello world"; + + let file = stor.create_file("hello.txt").unwrap(); + file.try_writer() + .unwrap() + .borrow_mut() + .write_all(content.as_bytes()) + .unwrap(); + + match stor.flush_file(&file) { + Ok(_) => { + let im_file = stor.files().get(file.path()).cloned(); + assert_eq!( + im_file, + Some(IMFile::File(InMemoryFile { + buf: content.as_bytes().to_vec(), + len: content.len() + })) + ); + } + _ => unreachable!(), + } + } + + #[test] + fn should_remove_a_file_in_read_mode() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + + let file = stor.write_file("hello.txt").unwrap(); + let file_path = file.path().to_path_buf(); + + match stor.remove_file(file) { + Ok(_) => { + let im_file = stor.files().get(&file_path).cloned(); + assert_eq!(im_file, None); + } + _ => unreachable!(), + } + } + + #[test] + fn should_remove_a_file_in_write_mode() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + + let file = stor.write_file("hello.txt").unwrap(); + let file_path = file.path().to_path_buf(); + + match stor.remove_file(file) { + Ok(_) => { + let im_file = stor.files().get(&file_path).cloned(); + assert_eq!(im_file, None); + } + _ => unreachable!(), + } + } + + #[test] + fn should_get_file_length() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + + let file = stor.read_file("hello.txt").unwrap(); + + match stor.file_len(&file) { + Ok(len) => { + let content = b"hello world".to_vec(); + assert_eq!(len, content.len()); + } + _ => unreachable!(), + } + } + + #[test] + fn should_open_dir() { + let stor = InMemoryStorage::default(); + stor.add_bar_foo_folder(); + + match stor.read_file("bar/foo/") { + Ok(Entry::Dir(path)) => assert_eq!(path, PathBuf::from("bar/foo/")), + _ => unreachable!(), + } + } + + #[test] + fn should_remove_dir_with_subfiles() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + stor.add_bar_foo_folder(); + + let file = stor.read_file("bar/foo/").unwrap(); + let file_path = file.path().to_path_buf(); + + match stor.remove_dir_all(file) { + Ok(()) => { + assert_eq!(stor.files().get(&file_path).cloned(), None); + let files = stor.files(); + let keys = files.keys().cloned().collect::>(); + assert_eq!( + sorted_file_names(&keys), + vec!["bar/", "bar/hello.txt", "bar/world.txt", "hello.txt"] + ); + } + _ => unreachable!(), + } + } + + #[test] + fn should_remove_dir_recursively_with_subfiles() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + stor.add_bar_foo_folder(); + + let file = stor.read_file("bar/").unwrap(); + let file_path = file.path().to_path_buf(); + + match stor.remove_dir_all(file) { + Ok(()) => { + assert_eq!(stor.files().get(&file_path).cloned(), None); + let files = stor.files(); + let keys = files.keys().cloned().collect::>(); + assert_eq!(sorted_file_names(&keys), vec!["hello.txt"]); + } + _ => unreachable!(), + } + } + + #[test] + fn should_return_file_names_of_dir_subfiles() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + stor.add_bar_foo_folder(); + + let file = stor.read_file("bar/").unwrap(); + + match stor.read_dir(&file) { + Ok(files) => { + let file_names = files + .iter() + .map(|f| f.path().to_path_buf()) + .collect::>(); + assert_eq!( + sorted_file_names(&file_names), + vec![ + "bar/", + "bar/foo/", + "bar/foo/hello.txt", + "bar/foo/world.txt", + "bar/hello.txt", + "bar/world.txt", + ] + ); + } + _ => unreachable!(), + } + } + + #[test] + fn should_include_hidden_files_names() { + let stor = InMemoryStorage::default(); + stor.add_hello_txt(); + stor.add_bar_foo_folder_with_hidden(); + + let file = stor.read_file("bar/").unwrap(); + + match stor.read_dir(&file) { + Ok(files) => { + let file_names = files + .into_iter() + .map(|f| f.path().to_path_buf()) + .collect::>(); + assert_eq!( + sorted_file_names(&file_names), + vec![ + "bar/", + "bar/.foo/", + "bar/.foo/hello.txt", + "bar/.foo/world.txt", + "bar/.hello.txt", + "bar/world.txt", + ] + ); + } + _ => unreachable!(), + } + } +} diff --git a/crates/volaris-domain/src/unpack.rs b/crates/volaris-domain/src/unpack.rs new file mode 100644 index 0000000..61f3142 --- /dev/null +++ b/crates/volaris-domain/src/unpack.rs @@ -0,0 +1,177 @@ +//! This contains the logic for decrypting a zip file, and extracting each file to the target directory. The temporary zip file is then erased with one pass. +//! +//! This is known as "unpacking" within volaris. + +use std::cell::RefCell; +use std::io::{Read, Seek, Write}; +use std::path::PathBuf; +use std::sync::Arc; + +use crate::storage::{self, Storage}; +use crate::{decrypt, overwrite}; +use core::protected::Protected; + +#[derive(Debug)] +pub enum Error { + WriteData, + OpenArchive, + OpenArchivedFile, + ResetCursorPosition, + Storage(storage::Error), + Decrypt(decrypt::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::WriteData => f.write_str("Unable to write data"), + Error::OpenArchive => f.write_str("Unable to open archive"), + Error::OpenArchivedFile => f.write_str("Unable to open archived file"), + Error::ResetCursorPosition => f.write_str("Unable to reset cursor position"), + Error::Storage(inner) => write!(f, "Storage error: {inner}"), + Error::Decrypt(inner) => write!(f, "Decrypt error: {inner}"), + } + } +} + +impl std::error::Error for Error {} + +type OnArchiveInfo = Box; +type OnZipFileFn = Box bool>; + +pub struct Request<'a, R> +where + R: Read, +{ + pub reader: &'a RefCell, + pub header_reader: Option<&'a RefCell>, + pub raw_key: Protected>, + pub output_dir_path: PathBuf, + pub on_decrypted_header: Option, + pub on_archive_info: Option, + pub on_zip_file: Option, +} + +pub fn execute( + stor: Arc + 'static>, + req: Request<'_, RW>, +) -> Result<(), Error> { + // 1. Create temp zip archive. + let tmp_file = stor.create_temp_file().map_err(Error::Storage)?; + + // 2. Decrypt input file to temp zip archive. + decrypt::execute(decrypt::Request { + header_reader: req.header_reader, + reader: req.reader, + writer: tmp_file + .try_writer() + .expect("We sure that file in write mode"), + raw_key: req.raw_key, + on_decrypted_header: req.on_decrypted_header, + }) + .map_err(Error::Decrypt)?; + + let buf_capacity = stor.file_len(&tmp_file).map_err(Error::Storage)?; + + // 3. Recover files from temp archive. + { + let mut reader = tmp_file + .try_reader() + .expect("We sure that file in read mode") + .borrow_mut(); + + reader.rewind().map_err(|_| Error::ResetCursorPosition)?; + + let mut archive = zip::ZipArchive::new(&mut *reader).map_err(|_| Error::OpenArchive)?; + + let output_dir = req.output_dir_path.clone(); + + // 4. prepare phase + let entities = (0..archive.len()) + .filter_map(|i| { + let zip_file = archive.by_index(i).ok()?; + let mut full_path = output_dir.clone(); + + // Prevent zip slip attack + // + // Source: https://snyk.io/research/zip-slip-vulnerability + zip_file.enclosed_name().map(|path| { + full_path.push(path); + + (full_path, i, zip_file.is_dir()) + }) + }) + .filter(|(full_path, ..)| { + if let Some(on_zip_file) = req.on_zip_file.as_ref() { + on_zip_file(full_path.clone()) + } else { + true + } + }) + .collect::>(); + + let files_count = entities.len(); + if let Some(on_archive_info) = req.on_archive_info { + on_archive_info(files_count); + } + + // 5. create dirs + #[allow(clippy::needless_collect)] + let create_dirs_jobs = entities + .iter() + .filter(|(_, _, is_dir)| *is_dir) + .map(|(fp, ..)| fp) + .chain([&output_dir]) + .map(|full_path| { + let stor = stor.clone(); + let full_path = full_path.clone(); + std::thread::spawn(move || stor.create_dir_all(full_path).map_err(Error::Storage)) + }) + .collect::>(); + + create_dirs_jobs + .into_iter() + .try_for_each(|th| th.join().unwrap())?; + + // 6. create files + entities + .iter() + .filter(|(_, _, is_dir)| !*is_dir) + .try_for_each(|(full_path, i, _)| { + let mut zip_file = archive.by_index(*i).map_err(|_| Error::OpenArchivedFile)?; + let file = stor + .create_file(full_path) + .or_else(|_| stor.write_file(full_path)) + .map_err(Error::Storage)?; + std::io::copy( + &mut zip_file, + &mut *file.try_writer().map_err(Error::Storage)?.borrow_mut(), + ) + .map_err(|_| Error::WriteData)?; + Ok(()) + })?; + } + + // 7. Finally eraze temp zip archive with zeros. + overwrite::execute(overwrite::Request { + buf_capacity, + writer: tmp_file + .try_writer() + .expect("We sure that file in write mode"), + passes: 1, + }) + .ok(); + + stor.remove_file(tmp_file).ok(); + + Ok(()) +} + +#[cfg(test)] +mod tests { + #[test] + #[ignore = "not yet implemented"] + fn should_unpack_encrypted_archive() { + todo!() + } +} diff --git a/crates/volaris-domain/src/utils.rs b/crates/volaris-domain/src/utils.rs new file mode 100644 index 0000000..17a2889 --- /dev/null +++ b/crates/volaris-domain/src/utils.rs @@ -0,0 +1,56 @@ +// TODO(pleshevskiy): dedup these utils + +#[cfg(test)] +mod test { + use core::primitives::{get_nonce_len, Algorithm, Mode, MASTER_KEY_LEN, SALT_LEN}; + use core::protected::Protected; + use rand::{prelude::StdRng, RngCore, SeedableRng}; + + const SALT_SEED: u64 = 123_456; + const NONCE_SEED: u64 = SALT_SEED + 1; + const MASTER_KEY_SEED: u64 = NONCE_SEED + 2; + + #[must_use] + pub fn gen_salt() -> [u8; SALT_LEN] { + let mut salt = [0u8; SALT_LEN]; + StdRng::seed_from_u64(SALT_SEED).fill_bytes(&mut salt); + salt + } + + #[must_use] + pub fn gen_nonce(algorithm: &Algorithm, mode: &Mode) -> Vec { + let nonce_len = get_nonce_len(algorithm, mode); + let mut nonce = vec![0u8; nonce_len]; + StdRng::seed_from_u64(NONCE_SEED).fill_bytes(&mut nonce); + nonce + } + + #[must_use] + pub fn gen_master_key() -> Protected<[u8; MASTER_KEY_LEN]> { + let mut master_key = [0u8; MASTER_KEY_LEN]; + StdRng::seed_from_u64(MASTER_KEY_SEED).fill_bytes(&mut master_key); + Protected::new(master_key) + } +} + +#[must_use] +pub fn hex_encode(bytes: &[u8]) -> String { + bytes + .iter() + .map(|b| format!("{b:02x}")) + .collect::() +} + +#[cfg(test)] +pub use test::gen_master_key; +#[cfg(test)] +pub use test::gen_nonce; +#[cfg(test)] +pub use test::gen_salt; + +#[cfg(not(test))] +pub use core::primitives::gen_master_key; +#[cfg(not(test))] +pub use core::primitives::gen_nonce; +#[cfg(not(test))] +pub use core::primitives::gen_salt; diff --git a/crates/volaris-domain/tests/common.rs b/crates/volaris-domain/tests/common.rs new file mode 100644 index 0000000..97ac734 --- /dev/null +++ b/crates/volaris-domain/tests/common.rs @@ -0,0 +1,89 @@ +use volaris_domain::storage::{Error, FileStorage, Storage}; +use std::fs; +use std::io::Write; +use std::ops::Deref; +use std::path::{Path, PathBuf}; + +pub struct TestFileStorage { + inner: FileStorage, + test_case_n: u32, +} + +impl TestFileStorage { + pub fn new(test_case_n: u32) -> Self { + Self { + inner: FileStorage, + test_case_n, + } + } +} + +impl Deref for TestFileStorage { + type Target = FileStorage; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Drop for TestFileStorage { + fn drop(&mut self) { + fs::remove_file(format!("hello_{}.txt", self.test_case_n)).ok(); + fs::remove_dir_all(format!("bar_{}/", self.test_case_n)).ok(); + } +} + +pub fn save_text_file

(stor: &TestFileStorage, path: P, content: &str) -> Result<(), Error> +where + P: AsRef, +{ + let file = stor.create_file(path)?; + file.try_writer()? + .borrow_mut() + .write_all(content.as_bytes()) + .map_err(|_| Error::CreateFile)?; + stor.flush_file(&file) +} + +// -------------------------------- +// TEST DATA +// ------------------------------- + +pub fn add_hello_txt(stor: &TestFileStorage) -> Result<(), Error> { + save_text_file( + stor, + format!("hello_{}.txt", stor.test_case_n), + "hello world", + ) +} + +pub fn add_bar_foo_folder(stor: &TestFileStorage) -> Result<(), Error> { + let bar_dir = format!("bar_{}", stor.test_case_n); + fs::create_dir(&bar_dir).map_err(|_| Error::CreateFile)?; + save_text_file(stor, format!("{}/hello.txt", &bar_dir), "hello")?; + save_text_file(stor, format!("{}/world.txt", &bar_dir), "world")?; + fs::create_dir(format!("{}/foo/", &bar_dir)).map_err(|_| Error::CreateFile)?; + save_text_file(stor, format!("{}/foo/hello.txt", &bar_dir), "hello")?; + save_text_file(stor, format!("{}/foo/world.txt", &bar_dir), "world")?; + Ok(()) +} + +pub fn add_bar_foo_folder_with_hidden(stor: &TestFileStorage) -> Result<(), Error> { + let bar_dir = format!("bar_{}", stor.test_case_n); + fs::create_dir(&bar_dir).map_err(|_| Error::CreateFile)?; + save_text_file(stor, format!("{}/.hello.txt", &bar_dir), "hello")?; + save_text_file(stor, format!("{}/world.txt", &bar_dir), "world")?; + fs::create_dir(format!("{}/.foo/", &bar_dir)).map_err(|_| Error::CreateFile)?; + save_text_file(stor, format!("{}/.foo/hello.txt", &bar_dir), "hello")?; + save_text_file(stor, format!("{}/.foo/world.txt", &bar_dir), "world")?; + Ok(()) +} + +pub fn sorted_file_names(file_names: Vec<&PathBuf>) -> Vec<&str> { + let mut keys = file_names + .iter() + .map(|k| k.to_str().unwrap()) + .collect::>(); + keys.sort_unstable(); + keys +} diff --git a/crates/volaris-domain/tests/storage.rs b/crates/volaris-domain/tests/storage.rs new file mode 100644 index 0000000..6588e85 --- /dev/null +++ b/crates/volaris-domain/tests/storage.rs @@ -0,0 +1,277 @@ +mod common; +use common::*; +use volaris_domain::storage::*; +use std::fs; +use std::io::{Read, Write}; +use std::path::PathBuf; + +#[test] +fn should_create_a_new_file() { + let stor = TestFileStorage::new(1); + + match stor.create_file("hello_1.txt") { + Ok(_) => match fs::File::open("hello_1.txt") { + Ok(_) => {} + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +#[test] +fn should_throw_an_error_if_file_already_exist() { + let stor = TestFileStorage::new(2); + add_hello_txt(&stor).unwrap(); + + match stor.create_file("hello_2.txt") { + Err(Error::CreateFile) => {} + _ => unreachable!(), + } +} + +#[test] +fn should_not_open_file_to_read() { + let stor = TestFileStorage::new(3); + + match stor.read_file("hello_3.txt") { + Err(Error::OpenFile(FileMode::Read)) => {} + _ => unreachable!(), + } +} + +#[test] +fn should_not_open_file_to_write() { + let stor = TestFileStorage::new(4); + + match stor.write_file("hello_4.txt") { + Err(Error::OpenFile(FileMode::Write)) => {} + _ => unreachable!(), + } +} + +#[test] +fn should_open_exist_file_in_read_mode() { + let stor = TestFileStorage::new(5); + add_hello_txt(&stor).unwrap(); + + match stor.read_file("hello_5.txt") { + Ok(file) => { + let mut file_buf = vec![]; + file.try_reader() + .unwrap() + .borrow_mut() + .read_to_end(&mut file_buf) + .unwrap(); + let content = b"hello world".to_vec(); + assert_eq!(file_buf, content); + assert_eq!(file_buf.len(), content.len()); + } + _ => unreachable!(), + } +} + +#[test] +fn should_open_exist_file_in_write_mode() { + let stor = TestFileStorage::new(6); + add_hello_txt(&stor).unwrap(); + + match stor.write_file("hello_6.txt") { + Ok(file) => { + file.try_writer() + .unwrap() + .borrow_mut() + .write_all(b"hello") + .unwrap(); + stor.flush_file(&file).unwrap(); + let mut file_buf = vec![]; + fs::File::open("hello_6.txt") + .unwrap() + .read_to_end(&mut file_buf) + .unwrap(); + let content = b"hello".to_vec(); + //assert_eq!(file_buf.len(), content.len()); + assert_eq!(file_buf, content); + } + _ => unreachable!(), + } +} + +#[test] +fn should_write_content_to_file() { + let stor = TestFileStorage::new(7); + let content = "hello world"; + + let file = stor.create_file("hello_7.txt").unwrap(); + file.try_writer() + .unwrap() + .borrow_mut() + .write_all(content.as_bytes()) + .unwrap(); + + match stor.flush_file(&file) { + Ok(_) => { + let mut file_buf = vec![]; + fs::File::open("hello_7.txt") + .unwrap() + .read_to_end(&mut file_buf) + .unwrap(); + let content = b"hello world".to_vec(); + assert_eq!(file_buf, content); + assert_eq!(file_buf.len(), content.len()); + } + _ => unreachable!(), + } +} + +#[test] +fn should_remove_a_file_in_read_mode() { + let stor = TestFileStorage::new(8); + add_hello_txt(&stor).unwrap(); + + let file = stor.write_file("hello_8.txt").unwrap(); + + match stor.remove_file(file) { + Ok(_) => match fs::File::open("hello_8.txt") { + Err(_) => {} + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +#[test] +fn should_remove_a_file_in_write_mode() { + let stor = TestFileStorage::new(9); + add_hello_txt(&stor).unwrap(); + + let file = stor.write_file("hello_9.txt").unwrap(); + + match stor.remove_file(file) { + Ok(_) => match fs::File::open("hello_9.txt") { + Err(_) => {} + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +#[test] +fn should_get_file_length() { + let stor = TestFileStorage::new(10); + add_hello_txt(&stor).unwrap(); + + let file = stor.read_file("hello_10.txt").unwrap(); + + match stor.file_len(&file) { + Ok(len) => { + let content = b"hello world".to_vec(); + assert_eq!(len, content.len()); + } + _ => unreachable!(), + } +} + +#[test] +fn should_open_dir() { + let stor = TestFileStorage::new(11); + add_bar_foo_folder(&stor).unwrap(); + + match stor.read_file("bar_11/foo/") { + Ok(Entry::Dir(path)) => assert_eq!(path, PathBuf::from("bar_11/foo/")), + _ => unreachable!(), + } +} + +#[test] +fn should_remove_dir_with_subfiles() { + let stor = TestFileStorage::new(12); + add_hello_txt(&stor).unwrap(); + add_bar_foo_folder(&stor).unwrap(); + + let file = stor.read_file("bar_12/foo/").unwrap(); + + match stor.remove_dir_all(file) { + Ok(()) => match (fs::read_dir("bar_12/"), fs::read_dir("bar_12/foo/")) { + (Ok(_), Err(_)) => {} + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +#[test] +fn should_remove_dir_recursively_with_subfiles() { + let stor = TestFileStorage::new(13); + add_hello_txt(&stor).unwrap(); + add_bar_foo_folder(&stor).unwrap(); + + let file = stor.read_file("bar_13/").unwrap(); + + match stor.remove_dir_all(file) { + Ok(()) => match (fs::read_dir("bar_13/"), fs::read_dir("bar_13/foo/")) { + (Err(_), Err(_)) => {} + _ => unreachable!(), + }, + _ => unreachable!(), + } +} + +#[test] +fn should_return_file_names_of_dir_subfiles() { + let stor = TestFileStorage::new(14); + add_hello_txt(&stor).unwrap(); + add_bar_foo_folder(&stor).unwrap(); + + let file = stor.read_file("bar_14/").unwrap(); + + match stor.read_dir(&file) { + Ok(files) => { + let file_names = files + .iter() + .map(|f| f.path().to_path_buf()) + .collect::>(); + assert_eq!( + sorted_file_names(file_names.iter().collect()), + vec![ + "bar_14/", + "bar_14/foo", + "bar_14/foo/hello.txt", + "bar_14/foo/world.txt", + "bar_14/hello.txt", + "bar_14/world.txt", + ] + ) + } + _ => unreachable!(), + } +} + +#[test] +fn should_include_hidden_files_names() { + let stor = TestFileStorage::new(15); + add_hello_txt(&stor).unwrap(); + add_bar_foo_folder_with_hidden(&stor).unwrap(); + + let file = stor.read_file("bar_15/").unwrap(); + + match stor.read_dir(&file) { + Ok(files) => { + let file_names = files + .iter() + .map(|f| f.path().to_path_buf()) + .collect::>(); + assert_eq!( + sorted_file_names(file_names.iter().collect()), + vec![ + "bar_15/", + "bar_15/.foo", + "bar_15/.foo/hello.txt", + "bar_15/.foo/world.txt", + "bar_15/.hello.txt", + "bar_15/world.txt", + ] + ) + } + _ => unreachable!(), + } +}