diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e1d7517..abb187a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,9 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ key: server-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + - if: runner.os == 'Linux' + name: Install additional packages + run: sudo apt update && sudo apt install -y libudev-dev - name: Build run: cargo build -r - name: Lint diff --git a/Cargo.lock b/Cargo.lock index 0be2594..91a0ca6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" dependencies = [ "cipher", "cpubits", @@ -35,6 +35,7 @@ dependencies = [ "ctr", "ctutils", "polyval", + "zeroize", ] [[package]] @@ -82,7 +83,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -93,9 +94,15 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "argon2" version = "0.6.0" @@ -106,6 +113,46 @@ dependencies = [ "blake2", "cpufeatures", "password-hash", + "zeroize", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.21", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -114,6 +161,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -138,12 +191,27 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bzip2" version = "0.6.1" @@ -153,11 +221,20 @@ dependencies = [ "libbz2-rs-sys", ] +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher", +] + [[package]] name = "cc" -version = "1.4.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "shlex", @@ -171,9 +248,9 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "chacha20" @@ -181,10 +258,11 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "cipher", "cpufeatures", "rand_core", + "zeroize", ] [[package]] @@ -197,6 +275,7 @@ dependencies = [ "chacha20", "cipher", "poly1305", + "zeroize", ] [[package]] @@ -212,6 +291,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.5.2" @@ -225,9 +331,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -235,9 +341,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -247,21 +353,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.8", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cmov" @@ -304,22 +410,28 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "cryptcrypt" -version = "0.5.1" +version = "0.6.0" dependencies = [ "aes-gcm-siv", "argon2", @@ -328,6 +440,7 @@ dependencies = [ "chrono", "clap", "crossbeam-channel", + "ctap-hid-fido2", "drill-press", "filetime", "hkdf", @@ -342,7 +455,8 @@ dependencies = [ "typed-path", "typenum", "walkdir", - "windows-sys", + "windows-sys 0.61.2", + "zeroize", ] [[package]] @@ -351,11 +465,33 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "getrandom", + "getrandom 0.4.3", "hybrid-array", "rand_core", ] +[[package]] +name = "ctap-hid-fido2" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2cee48715b2a8fae3739aeb8433817051a6772562ba67f9b631fc833eb793" +dependencies = [ + "aes", + "anyhow", + "base64", + "byteorder", + "cbc", + "ciborium", + "hex", + "hidapi", + "num", + "pad", + "ring", + "strum", + "strum_macros", + "x509-parser", +] + [[package]] name = "ctr" version = "0.10.1" @@ -367,13 +503,39 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" dependencies = [ "cmov", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "digest" version = "0.11.3" @@ -386,6 +548,17 @@ dependencies = [ "ctutils", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.8", +] + [[package]] name = "drill-press" version = "0.1.2" @@ -395,7 +568,7 @@ dependencies = [ "cfg-if 0.1.10", "errno", "libc", - "thiserror", + "thiserror 1.0.69", "winapi", ] @@ -426,15 +599,15 @@ version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "libc", ] [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "futures-core" @@ -460,18 +633,40 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if 1.0.5", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "libc", "r-efi", "rand_core", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if 1.0.5", + "crunchy", + "zerocopy", +] + [[package]] name = "heck" version = "0.5.0" @@ -480,9 +675,28 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hidapi" +version = "2.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818c0e1d27887aaf76fe737042e27a66b796a7b099e6d2e1a72d106c2dff3fa6" +dependencies = [ + "cc", + "cfg-if 1.0.5", + "libc", + "pkg-config", + "windows-sys 0.61.2", +] [[package]] name = "hkdf" @@ -504,9 +718,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] @@ -541,6 +755,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ + "block-padding", "hybrid-array", ] @@ -552,11 +767,11 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "js-sys" -version = "0.3.104" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "futures-util", "wasm-bindgen", ] @@ -567,10 +782,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "cpufeatures", ] +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + [[package]] name = "libbz2-rs-sys" version = "0.2.5" @@ -579,9 +800,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "log" @@ -589,6 +810,97 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -608,6 +920,15 @@ dependencies = [ "libc", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -620,6 +941,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "pad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width", +] + [[package]] name = "parse-size" version = "1.1.0" @@ -632,7 +962,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" dependencies = [ - "getrandom", + "getrandom 0.4.3", "phc", ] @@ -644,7 +974,7 @@ checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" dependencies = [ "base64ct", "ctutils", - "getrandom", + "getrandom 0.4.3", ] [[package]] @@ -653,6 +983,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + [[package]] name = "poly1305" version = "0.9.1" @@ -674,6 +1010,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -709,12 +1051,12 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", - "getrandom", + "getrandom 0.4.3", "rand_core", ] @@ -734,6 +1076,20 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if 1.0.5", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rpassword" version = "7.5.4" @@ -742,7 +1098,7 @@ checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" dependencies = [ "libc", "rtoolbox", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -752,7 +1108,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", ] [[package]] @@ -779,13 +1144,43 @@ dependencies = [ "zeroize", ] +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.8", +] + [[package]] name = "sha2" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "cpufeatures", "digest", ] @@ -825,6 +1220,24 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "syn" version = "2.0.119" @@ -838,22 +1251,42 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "01016da373cd8f7ef12624f796309f5c31ba8d646dd08856c02cd741d823c622" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", ] [[package]] @@ -867,6 +1300,47 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.8", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typed-path" version = "0.12.3" @@ -881,9 +1355,15 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-width" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "universal-hash" @@ -895,6 +1375,12 @@ dependencies = [ "ctutils", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "utf8parse" version = "0.2.2" @@ -911,13 +1397,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" -version = "0.2.127" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -926,9 +1418,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.127" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -936,22 +1428,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.127" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.8", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.127" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] @@ -978,7 +1470,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1046,6 +1538,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1055,20 +1556,101 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.21", + "time", +] + [[package]] name = "zerocopy" -version = "0.8.56" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.56" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" dependencies = [ "proc-macro2", "quote", @@ -1077,6 +1659,20 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/Cargo.toml b/Cargo.toml index f1e5a6b..7494451 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,17 @@ [package] name = "cryptcrypt" -version = "0.5.1" +version = "0.6.0" edition = "2024" [dependencies] -aes-gcm-siv = "0.12.0" -argon2 = "0.6.0" +aes-gcm-siv = { version = "0.12.0", features = ["zeroize"] } +argon2 = { version = "0.6.0", features = ["zeroize"] } bzip2 = "0.6.1" -chacha20poly1305 = "0.11.0" +chacha20poly1305 = { version = "0.11.0", features = ["zeroize"] } chrono = "0.4.45" clap = { version = "4.5.60", features = ["derive"] } crossbeam-channel = "0.5.15" +ctap-hid-fido2 = "3.6.0" drill-press = "0.1.2" filetime = "0.2.29" hkdf = "0.13.0" @@ -26,6 +27,7 @@ typed-path = "0.12.3" typenum = "1.19.0" walkdir = "2.5.0" windows-sys = { version = "0.61.2", features = ["Win32_System_Ioctl"] } +zeroize = { version = "1.9.0", features = ["zeroize_derive"] } [profile.release] lto = "thin" diff --git a/README.md b/README.md index d7a14a6..862a94c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # cryptcrypt A command-line tool for encrypting and decrypting a file or a directory using modern encryption algorithms and password-based key derivation. -Additionally, it can use a key file, bzip2 compression and an output file split. The contents of a directory are concatenated into an archive. +Additionally, it can use a USB security key, a key file, bzip2 compression and an output file split. The contents of a directory are +concatenated into an archive. **Security Notes**: This software is experimental. There wasn't any security audit. Do not use in production. USE AT YOUR OWN RISK! @@ -9,7 +10,8 @@ Additionally, it can use a key file, bzip2 compression and an output file split. - Dual encryption with ciphers AES-GCM-SIV and XChaCha20Poly1305 - Password-based key derivation (Argon2id) -- Optional key file can be used additionally to the password +- Optional USB security key / FIDO authenticator to supplement the password +- Optional key file (its first 64 MB) can be used additionally to the password - Optional bzip2 compression before encryption - Optional split of output into files of user-defined number and size - Handles large files efficiently by parallel processing them in chunks @@ -27,6 +29,15 @@ Additionally, it can use a key file, bzip2 compression and an output file split. - Stores relative paths - Building an archive (i.e. reading files) is done in parallel for improved performance +### USB security key +- FIDO2 authenticator +- PIN must be set on device and need to be entered by user +- Non-discoverable (non-resident) credentials +- Hmac-secret +- Tested with Token2 PIN+ Release3.3 + +An encrypted file can be bound to only one USB security key. If you want to protect data against hardware failure of USB security key, you should encrypt the data again with another USB security key. + ## Usage ``` @@ -44,6 +55,7 @@ Options: -o, --out-dir Output directory, it is created if it does not exist -d, --decrypt Decrypt file (with extension '.cce' or for split series '.c00') -k, --keyfile Additional key file to supplement the password + -u, --usb-key Additional USB security key/FIDO authenticator to supplement the password, automatically detected on decryption -z, --compress Compress data before encryption, automatically detected on decryption -s, --split Split encrypted data into pieces of binary byte sizes (e.g. 2g,3g,1g) [G|g|M|m|K|k] -l, --list-archive List elements of an archive file, do not create its elements @@ -94,21 +106,29 @@ cargo run --release ## Encryption details -1. If a key file is used, hash its first 64 MByte (maximum) with [sha3-512](https://github.com/RustCrypto/hashes/tree/master/sha3). -2. Derive encryption key from password and the optional key file hash using [Argon2id](https://github.com/RustCrypto/password-hashes/tree/master/argon2) with a random salt. Write the salt to the output file. -3. From the master key derive two independent 32‑byte keys: one for XChaCha20-Poly1305 and one for AES-256-GCM-SIV. -Each key is derived with [HKDF-SHA256](https://github.com/RustCrypto/KDFs/tree/master/hkdf) using its own fresh random salt. -Write the ChaCha salt then the AES salt immediately after the password salt. -4. Encrypt and write file format version and file format (i.e. compression and archive status) to the output file. (file header: password salt, ChaCha salt, AES salt, encrypted file format version, encrypted file format). -5. Read a 1 MByte chunk from the input file (the last chunk may be smaller). Keep a zero-based sequence number for each chunk and mark the final chunk with a flag. -6. If compression is switched on, compress chunk with [bzip2](https://github.com/trifectatechfoundation/bzip2-rs). Its variably sized output is copied to fixed sized, 1 MB chunks. The chunk size should not give a hint about the cleartext. -7. First-pass encrypt the chunk with [XChaCha20-Poly1305](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305) -using a fresh random base nonce generated per chunk. For the actual encryption nonce the implementation derives a per-chunk nonce -by XOR’ing the base nonce with the chunk’s sequence number and the final-chunk flag; the original base nonce is stored after the -ChaCha ciphertext so the per-chunk nonce can be recomputed during decryption. Reordering or truncating the chunk sequence would cause a decryption error. -8. Second-pass encrypt the output of step 7 with [AES-256-GCM-SIV](https://github.com/RustCrypto/AEADs/tree/master/aes-gcm-siv) -using a fresh random nonce; the AES nonce is stored after the AES ciphertext and written to the (split) output file. -9. Repeat steps 5–8 until all input is processed. +The header of an encrypted file consists of an unencrypted part and an encrypted part: + +1. **Unencrypted header part:** + It contains two salts used to encrypt the second part of the header: + * A random salt used to create a key from the password with [Argon2id](https://github.com/RustCrypto/password-hashes/tree/master/argon2). + * A different random salt used to derive two keys from that password-derived key using [HKDF-SHA256](https://github.com/RustCrypto/KDFs/tree/master/hkdf). These two final keys are used to encrypt the second header part with [XChaCha20-Poly1305](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305) and [AES-256-GCM-SIV](https://github.com/RustCrypto/AEADs/tree/master/aes-gcm-siv). + +2. **Encrypted header part:** + It contains two new salts used to encrypt the file body and additional metadata: + * A random salt where the inputs to [Argon2id](https://github.com/RustCrypto/password-hashes/tree/master/argon2) for generating a primary key are: the password, an optional USB security key's hmac-secret, and an optional key file hash. + * Another random salt used to derive two keys from the primary key using [HKDF-SHA256](https://github.com/RustCrypto/KDFs/tree/master/hkdf). + These two keys are used to encrypt the file's body with [XChaCha20-Poly1305](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305) and [AES-256-GCM-SIV](https://github.com/RustCrypto/AEADs/tree/master/aes-gcm-siv). + + If a key file is used, its first 64 MB (maximum) is hashed with [sha3-512](https://github.com/RustCrypto/hashes/tree/master/sha3). If a USB security key is used, a credential ID and a random salt are stored and used to retrieve an hmac-secret from the key; otherwise, their space is filled with random data. Additionally, the following data and flags are saved: the file format version, and flags indicating whether compression, an archive, a key file, or a USB security key is used. The overall header length remains constant regardless of which options are selected. + +The encryption of the file's body follows this procedure: + +1. Read a 1 MB chunk from the input file (the last chunk may be smaller). Keep a zero-based sequence number for each chunk and mark the final chunk with a flag. +2. If compression is enabled, compress the chunk using [bzip2](https://github.com/trifectatechfoundation/bzip2-rs). Its variable-sized output is copied into fixed-size, 1 MB chunks. (The chunk size should not leak information about the cleartext.) +3. **First-pass encryption:** Encrypt the chunk with [XChaCha20-Poly1305](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305) using a fresh random base nonce generated per chunk. The implementation derives the actual encryption nonce by XOR'ing the base nonce with the chunk's sequence number and the final-chunk flag. The original base nonce is stored after the ChaCha ciphertext so the per-chunk nonce can be recomputed during decryption. Reordering or truncating the chunk sequence causes a decryption error. Additional associated data (AAD) used for authentication includes: the file format version, the string "Cha", and the file ID. +4. **Second-pass encryption:** Encrypt the output of step 3 with [AES-256-GCM-SIV](https://github.com/RustCrypto/AEADs/tree/master/aes-gcm-siv) using a fresh random nonce. AAD contains: the file format version, the string "Aes", the file ID, the chunk count, and the final flag. The AES nonce is stored after the AES ciphertext and written to the output file. +5. Repeat steps 1 through 4 until all input is processed. + ## License diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index edf4ef3..f9ef343 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -4,40 +4,49 @@ version = 4 [[package]] name = "aead" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common 0.1.7", - "generic-array", + "crypto-common", + "inout", ] [[package]] name = "aes" -version = "0.8.4" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" dependencies = [ - "cfg-if 1.0.4", "cipher", - "cpufeatures 0.2.17", + "cpubits", + "cpufeatures", ] [[package]] name = "aes-gcm-siv" -version = "0.11.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +checksum = "4c32325128cb24de2a2d14d92da7569dae7073f9e47334521d494747fa94d902" dependencies = [ "aead", "aes", "cipher", "ctr", + "ctutils", "polyval", - "subtle", "zeroize", ] +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -88,27 +97,85 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "arbitrary" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" dependencies = [ "derive_arbitrary", ] [[package]] name = "argon2" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.17", + "cpufeatures", "password-hash", + "zeroize", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.21", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", ] +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -117,31 +184,43 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "blake2" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] -name = "block-buffer" -version = "0.12.1" +name = "block-padding" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" dependencies = [ "hybrid-array", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bzip2" version = "0.6.1" @@ -151,11 +230,20 @@ dependencies = [ "libbz2-rs-sys", ] +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher", +] + [[package]] name = "cc" -version = "1.4.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b" dependencies = [ "find-msvc-tools", "jobserver", @@ -171,61 +259,92 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "chacha20" -version = "0.9.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if 1.0.4", - "cpufeatures 0.3.0", - "rand_core 0.10.1", + "cpufeatures", + "rand_core", + "zeroize", ] [[package]] name = "chacha20poly1305" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" dependencies = [ "aead", - "chacha20 0.9.1", + "chacha20", "cipher", "poly1305", "zeroize", ] +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "crypto-common 0.1.7", + "block-buffer", + "crypto-common", "inout", - "zeroize", ] [[package]] name = "clap" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -233,9 +352,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -245,21 +364,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.7", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cmov" @@ -280,48 +399,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "cryptcrypt" -version = "0.4.0" +version = "0.5.1" dependencies = [ "aes-gcm-siv", "argon2", "bzip2", "chacha20poly1305", + "chrono", "clap", "crossbeam-channel", + "ctap-hid-fido2", "drill-press", "filetime", "hkdf", @@ -337,78 +467,108 @@ dependencies = [ "typenum", "walkdir", "windows-sys 0.61.2", + "zeroize", ] [[package]] name = "cryptcrypt-fuzz" version = "0.0.0" dependencies = [ + "aes-gcm-siv", "arbitrary", + "chacha20poly1305", "cryptcrypt", "glob", "libfuzzer-sys", "rand", - "secrecy", ] [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", + "getrandom 0.4.3", + "hybrid-array", + "rand_core", ] [[package]] -name = "crypto-common" -version = "0.2.2" +name = "ctap-hid-fido2" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +checksum = "73e2cee48715b2a8fae3739aeb8433817051a6772562ba67f9b631fc833eb793" dependencies = [ - "hybrid-array", + "aes", + "anyhow", + "base64", + "byteorder", + "cbc", + "ciborium", + "hex", + "hidapi", + "num", + "pad", + "ring", + "strum", + "strum_macros", + "x509-parser", ] [[package]] name = "ctr" -version = "0.9.2" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ "cipher", ] [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "03bb0e1cc970d482d121d9a1744999169b69a07470b3d644a7894e53fcaf4574" dependencies = [ "cmov", ] [[package]] -name = "derive_arbitrary" -version = "1.4.2" +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der-parser" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", ] [[package]] -name = "digest" -version = "0.10.7" +name = "deranged" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac" dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", - "subtle", + "proc-macro2", + "quote", + "syn 3.0.7", ] [[package]] @@ -417,12 +577,23 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", + "block-buffer", "const-oid", - "crypto-common 0.2.2", + "crypto-common", "ctutils", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + [[package]] name = "drill-press" version = "0.1.2" @@ -432,7 +603,7 @@ dependencies = [ "cfg-if 0.1.10", "errno", "libc", - "thiserror", + "thiserror 1.0.69", "winapi", ] @@ -463,24 +634,38 @@ version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "libc", ] [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] -name = "generic-array" -version = "0.14.7" +name = "futures-core" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ - "typenum", - "version_check", + "futures-core", + "futures-task", + "pin-project-lite", + "slab", ] [[package]] @@ -489,7 +674,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "libc", "wasi", ] @@ -500,10 +685,10 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ - "cfg-if 1.0.4", + "cfg-if 1.0.5", "libc", "r-efi", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -512,6 +697,17 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if 1.0.5", + "crunchy", + "zerocopy", +] + [[package]] name = "heck" version = "0.5.0" @@ -520,9 +716,28 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hidapi" +version = "2.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "818c0e1d27887aaf76fe737042e27a66b796a7b099e6d2e1a72d106c2dff3fa6" +dependencies = [ + "cc", + "cfg-if 1.0.5", + "libc", + "pkg-config", + "windows-sys 0.61.2", +] [[package]] name = "hkdf" @@ -539,25 +754,50 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.3", + "digest", ] [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "inout" -version = "0.1.4" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ - "generic-array", + "block-padding", + "hybrid-array", ] [[package]] @@ -576,16 +816,33 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if 1.0.5", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "keccak" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ - "cfg-if 1.0.4", - "cpufeatures 0.3.0", + "cfg-if 1.0.5", + "cpufeatures", ] +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + [[package]] name = "libbz2-rs-sys" version = "0.2.5" @@ -594,9 +851,9 @@ checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libfuzzer-sys" @@ -608,6 +865,112 @@ dependencies = [ "cc", ] +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.17.0" @@ -618,6 +981,21 @@ dependencies = [ "libc", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -625,10 +1003,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] -name = "opaque-debug" -version = "0.3.1" +name = "pad" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width", +] [[package]] name = "parse-size" @@ -638,38 +1019,64 @@ checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" [[package]] name = "password-hash" -version = "0.5.0" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "getrandom 0.4.3", + "phc", +] + +[[package]] +name = "phc" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" dependencies = [ "base64ct", - "rand_core 0.6.4", - "subtle", + "ctutils", + "getrandom 0.4.3", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + [[package]] name = "poly1305" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", + "cpufeatures", "universal-hash", ] [[package]] name = "polyval" -version = "0.6.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" dependencies = [ - "cfg-if 1.0.4", - "cpufeatures 0.2.17", - "opaque-debug", + "cpubits", + "cpufeatures", "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -705,13 +1112,13 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ - "chacha20 0.10.1", + "chacha20", "getrandom 0.4.3", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -721,23 +1128,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" dependencies = [ "ppv-lite86", - "rand_core 0.10.1", + "rand_core", ] [[package]] name = "rand_core" -version = "0.6.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "rand_core" -version = "0.10.1" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if 1.0.5", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] [[package]] name = "rpassword" @@ -752,14 +1164,29 @@ dependencies = [ [[package]] name = "rtoolbox" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844" +checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "same-file" version = "1.0.6" @@ -778,15 +1205,45 @@ dependencies = [ "zeroize", ] +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + [[package]] name = "sha2" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ - "cfg-if 1.0.4", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cfg-if 1.0.5", + "cpufeatures", + "digest", ] [[package]] @@ -795,7 +1252,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ - "digest 0.11.3", + "digest", "keccak", "sponge-cursor", ] @@ -806,6 +1263,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "sponge-cursor" version = "0.1.0" @@ -819,10 +1282,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "subtle" -version = "2.6.1" +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + +[[package]] +name = "strum_macros" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "syn" @@ -837,22 +1312,42 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", ] [[package]] @@ -866,6 +1361,47 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "typed-path" version = "0.12.3" @@ -880,31 +1416,37 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-width" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "universal-hash" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common 0.1.7", - "subtle", + "crypto-common", + "ctutils", ] [[package]] -name = "utf8parse" -version = "0.2.2" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "version_check" -version = "0.9.5" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "walkdir" @@ -922,6 +1464,51 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if 1.0.5", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.7", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -953,17 +1540,70 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] @@ -1041,20 +1681,37 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.21", + "time", +] + [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" dependencies = [ "proc-macro2", "quote", @@ -1063,6 +1720,20 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index e0cc7e3..c520c05 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -13,7 +13,8 @@ cryptcrypt = { path = ".." } arbitrary = { version = "1", features = ["derive"] } glob = "0.3.3" rand = "0.10.1" -secrecy = "0.10.3" +chacha20poly1305 = { version = "0.11.0", features = ["zeroize"] } +aes-gcm-siv = { version = "0.12.0", features = ["zeroize"] } [[bin]] name = "fuzzer_encdec" diff --git a/fuzz/fuzz_targets/fuzzer_chunk_encdec.rs b/fuzz/fuzz_targets/fuzzer_chunk_encdec.rs index 1ef698e..a38afec 100644 --- a/fuzz/fuzz_targets/fuzzer_chunk_encdec.rs +++ b/fuzz/fuzz_targets/fuzzer_chunk_encdec.rs @@ -1,26 +1,33 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use secrecy::SecretSlice; use cryptcrypt::encryption::Encryption; use cryptcrypt::decryption::Decryption; - +use chacha20poly1305::XChaCha20Poly1305; +use aes_gcm_siv::Aes256GcmSiv; +use aes_gcm_siv::KeyInit; // fuzzing of compression, encryption, decryption of chunks fuzz_target!(|data: &[u8]| { let key = [42u8; 32]; - let key = SecretSlice::from(key.to_vec()); + let cipher_cha = XChaCha20Poly1305::new_from_slice(&key).unwrap(); + let cipher_aes = Aes256GcmSiv::new_from_slice(&key).unwrap(); let chunk_count = data.get(0).copied().unwrap_or(0); let final_flag = (chunk_count & 0x01) != 0; + let aad: &[u8] = if data.len() >= 33 { + &data[1..33] + } else { + &[] + }; - let zip_data = Encryption::compress_buffer(data).unwrap(); - let cha_enc = Encryption::cha_encrypt_buffer(&key, &zip_data, chunk_count.into(), final_flag).unwrap(); - let aes_enc = Encryption::aes_encrypt_buffer(&key, &cha_enc).unwrap(); + let mut enc_data = Encryption::compress_buffer(data).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut enc_data, chunk_count.into(), final_flag, aad).unwrap(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut enc_data, chunk_count.into(), final_flag, aad).unwrap(); - let aes_dec = Decryption::aes_decrypt_buffer(&key, &aes_enc).unwrap(); - let cha_dec = Decryption::cha_decrypt_buffer(&key, &aes_dec, chunk_count.into(), final_flag).unwrap(); - let unzip_data = Decryption::decompress_buffer(&cha_dec).unwrap(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut enc_data, chunk_count.into(), final_flag, aad).unwrap(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut enc_data, chunk_count.into(), final_flag, aad).unwrap(); + let unzip_data = Decryption::decompress_buffer(&enc_data).unwrap(); assert_eq!(data, unzip_data); }); diff --git a/fuzz/fuzz_targets/fuzzer_encdec.rs b/fuzz/fuzz_targets/fuzzer_encdec.rs index 14deb49..a394a5f 100644 --- a/fuzz/fuzz_targets/fuzzer_encdec.rs +++ b/fuzz/fuzz_targets/fuzzer_encdec.rs @@ -58,7 +58,7 @@ fuzz_target!(|arb_in: FuzzInput| { } } - // add file extension of encrypted file depending on whether file should be split + // add file extension of encrypted file depending on whether file should be split let mut filepath_out = path_in.clone(); if arb_in.split.is_empty() { filepath_out.add_extension(ENCRYPTED_FILE_EXT); @@ -74,10 +74,10 @@ fuzz_target!(|arb_in: FuzzInput| { } // encrypt data and decrypt its output - Encryption::encrypt(&path_in, None, filepath_key.as_ref(), arb_in.compress, arb_in.split, false).unwrap(); - - let path_extract = base_dir.join( PathBuf::from(format!("out{:x}{:x}", random, now)) ); - Decryption::decrypt(&filepath_out, Some(&path_extract), filepath_key.as_ref(), false).unwrap(); + Encryption::encrypt(&path_in, Some(&base_dir), filepath_key.as_ref(), false, arb_in.compress, arb_in.split, false).unwrap(); + + let path_extract = base_dir.join( PathBuf::from(format!("out{:x}{:x}", random, now)) ); + Decryption::decrypt(&filepath_out, Some(&path_extract), filepath_key.as_ref(), false, false).unwrap(); // clean up, delete files diff --git a/src/common.rs b/src/common.rs index 1b02243..16d3579 100644 --- a/src/common.rs +++ b/src/common.rs @@ -6,129 +6,401 @@ use hkdf::Hkdf; use sha2::{Digest, Sha256}; use sha3::Sha3_512; use secrecy::{ExposeSecret, ExposeSecretMut, SecretSlice, SecretString}; +use zeroize::{Zeroize, Zeroizing, ZeroizeOnDrop}; +use argon2::Argon2; +use chacha20poly1305::XChaCha20Poly1305; +use aes_gcm_siv::{Aes256GcmSiv, Nonce, KeySizeUser, aead::{Aead, Generate, Key, KeyInit}}; +use typenum::Unsigned; +use ctap_hid_fido2::{ + fidokey::{ + get_info::{InfoParam, InfoOption}, + CredentialExtension, MakeCredentialArgsBuilder, AssertionExtension, GetAssertionArgsBuilder + }, + Cfg, FidoKeyHidFactory, verifier +}; use crate::{Result, CHUNK_SIZE, MAX_KEYFILE_CHUNKS, KEY_SIZE}; -/// Prompts the user to enter a password from the terminal. +/// Encrypted in-memory storage for secret data. /// -/// In test mode, returns a hardcoded test password. In normal mode, reads a password -/// from user input, optionally verifying it matches a repeated entry. -/// -/// # Arguments -/// - `verify`: If `true`, prompts the user to repeat the password and verifies they match. -/// If `false`, only prompts once. -/// -/// # Returns -/// - `Ok(password)` containing the user's password -/// - `Err` if passwords don't match (when verify=true) or on I/O error -fn get_password_from_user(verify: bool) -> Result { - let password = SecretString::from( - if cfg!(test) || cfg!(fuzzing) { - if !cfg!(fuzzing) { println!("!!! Test-password used !!!"); } - "abc123test".to_string() +/// Keeps plaintext out of memory as long as possible by storing it +/// in an encrypted buffer and zeroizing sensitive data on drop. +#[derive(Default, Zeroize, ZeroizeOnDrop)] +struct CryptMemory { + /// Key used to encrypt and decrypt the stored payload. + key: [u8; ::KeySize::USIZE], + /// Nonce used for the authenticated encryption operation. + nonce: Nonce, + /// Ciphertext produced from the original plaintext buffer. + data: Vec, + /// Original length of the plaintext before encryption. + cleartext_len: usize, +} + +impl CryptMemory { + /// Creates a new encrypted in-memory secret from the provided plaintext bytes. + /// + /// A fresh random key and nonce are generated for each instance, and the + /// plaintext is encrypted before it is stored in memory. + /// + /// # Arguments + /// - `buf_in`: Plaintext bytes to encrypt and store in memory. + /// + /// # Returns + /// - `Ok(Self)` containing an encrypted in-memory representation of the input + /// - `Err` if encryption fails + fn new(buf_in: &[u8]) -> Result { + let cleartext_len = buf_in.len(); + let key = Key::::generate(); + let cipher = Aes256GcmSiv::new(&key); + let nonce = Nonce::generate(); + let data = cipher.encrypt(&nonce, buf_in)?; + Ok( Self{key: key.into(), nonce, data, cleartext_len} ) + } + + /// Decrypts the stored ciphertext and returns the original plaintext bytes. + /// + /// This method authenticates and decrypts the payload using the internally + /// stored key and nonce. + /// + /// # Returns + /// - `Ok(buf_out)` containing the original decrypted bytes + /// - `Err` if authentication or decryption fails + fn decrypt(&mut self) -> Result> { + let cipher = Aes256GcmSiv::new_from_slice(&self.key)?; + let buf_out = cipher.decrypt(&self.nonce, self.data.as_slice())?; + Ok(buf_out) + } +} + +/// Creates file encryption/decryption ciphers. +#[derive(Default)] +pub struct KeyBuilder { + /// Whether the password should be entered twice for confirmation. + verify_password: bool, + /// Encrypted password retained between header and body cipher creation. + password: CryptMemory, + /// Argon2 cost parameters used for password hashing + argon2_params: argon2::Params, + /// FIDO2 credential ID used to request the authenticator's hmac-secret. + pub credential_id_fido: Option>, +} + +impl KeyBuilder { + pub fn new(verify_password: bool) -> Self { + let it = Self::default(); + let argon2_params = if cfg!(test) || cfg!(fuzzing) { + // Argon2 costs: default, faster, for testing + argon2::Params::default() } else { - prompt_password("Enter password: ")? - } - ); + // Argon2 costs: memory: 2 GB, number of passes: 2, degree of parallelism: 1 + argon2::Params::new(2_097_152, 2, 1 , Some(KEY_SIZE)).unwrap() + }; + Self { verify_password, argon2_params, ..it } + } - if verify { - let password_rep = SecretString::from( + /// Prompts the user to enter a password from the terminal. + /// + /// In test mode, returns a hardcoded test password. In normal mode, reads a password + /// from user input, optionally verifying it matches a repeated entry. + /// Password is encrypted, so it is harder to get it with a memory dump. If a USB security key is used, + /// the password would otherwise stay in memory for quite a long time as cleartext. + /// + /// # Returns + /// - `Err` if passwords don't match (when verify=true), I/O error or encryption error + fn get_password_from_user(&mut self) -> Result<()> { + let password = SecretString::from( if cfg!(test) || cfg!(fuzzing) { + if !cfg!(fuzzing) { println!("!!! Test-password used !!!"); } "abc123test".to_string() } else { - prompt_password("Repeat password: ")? + prompt_password("Enter password: ")? } ); - if password.expose_secret() != password_rep.expose_secret() { - return Err("Passwords do not match!".into()); + if self.verify_password { + let password_rep = SecretString::from( + if cfg!(test) || cfg!(fuzzing) { + "abc123test".to_string() + } else { + prompt_password("Repeat password: ")? + } + ); + + if password.expose_secret() != password_rep.expose_secret() { + return Err("Passwords do not match!".into()); + } } + + self.password = CryptMemory::new(password.expose_secret().as_bytes())?; + + Ok(()) } - Ok(password) -} + /// Prompts the user to enter a PIN for the USB security key from the terminal. + /// + /// # Returns + /// - `Ok(pin)` containing the pin + /// - `Err` if there is an I/O error + fn get_fido_pin_from_user() -> Result { + let pin = SecretString::from( + prompt_password("Enter PIN of USB security key: ")? + ); + Ok(pin) + } -/// Reads and hashes a key file using SHA3-512. -/// -/// Opens the key file and reads it in chunks (up to MAX_KEYFILE_CHUNKS chunks of CHUNK_SIZE -/// bytes each, i.e. max. 64 MB), updating a SHA3-512 hasher with each chunk. Returns the final -/// hash as a 64-byte array. If the file is smaller than the chunk buffer, it stops reading when -/// EOF is reached. -/// -/// # Arguments -/// - `keyfilepath`: Path to the key file to read and hash -/// -/// # Returns -/// - `Ok(hash)` containing the SHA3-512 hash -/// - `Err` if the file cannot be opened or read -fn read_and_hash_keyfile(keyfilepath: &PathBuf) -> Result<[u8; 64]> { - let mut f_in = File::open(keyfilepath)?; - let mut buf_in = vec![0u8; CHUNK_SIZE]; - let mut hasher = Sha3_512::new(); - - let mut count_chunks = MAX_KEYFILE_CHUNKS; - while count_chunks > 0 { - let count_in = f_in.read(&mut buf_in)?; - if count_in == 0 { - break; + /// Reads and hashes a key file using SHA3-512. + /// + /// Opens the key file and reads it in chunks (up to MAX_KEYFILE_CHUNKS chunks of CHUNK_SIZE + /// bytes each, i.e. max. 64 MB), updating a SHA3-512 hasher with each chunk. Returns the final + /// hash as a 64-byte array. If the file is smaller than the chunk buffer, it stops reading when + /// EOF is reached. + /// + /// # Arguments + /// - `keyfilepath`: Path to the key file to read and hash + /// + /// # Returns + /// - `Ok(hash)` containing the SHA3-512 hash + /// - `Err` if the file cannot be opened or read + fn read_and_hash_keyfile(keyfilepath: &PathBuf) -> Result<[u8; 64]> { + let mut f_in = File::open(keyfilepath)?; + let mut buf_in = vec![0u8; CHUNK_SIZE]; + let mut hasher = Sha3_512::new(); + + let mut count_chunks = MAX_KEYFILE_CHUNKS; + while count_chunks > 0 { + let count_in = f_in.read(&mut buf_in)?; + if count_in == 0 { + break; + } + hasher.update(&buf_in[..count_in]); + count_chunks -= 1; } - hasher.update(&buf_in[..count_in]); - count_chunks -= 1; + + let hash = hasher.finalize(); + Ok(hash.into()) } - let hash = hasher.finalize(); - Ok(hash.into()) -} + /// Retrieves an hmac-secret from the connected FIDO2 authenticator. + /// + /// Requires exactly one authenticator with the hmac-secret extension, + /// enforced user verification, and a PIN. If no credential ID is stored, + /// retrieves a credential ID from authenticator and saves it + /// in `self.credential_id_fido`. + /// + /// # Arguments + /// - `salt`: Salt supplied to the authenticator's hmac-secret extension + /// + /// # Returns + /// - `Ok(secret)` containing the authenticator's hmac-secret + /// - `Err` if the authenticator is missing or unsupported, PIN input or + /// device interaction fails, or the credential response is invalid + fn usb_fido2_hmac_secret(&mut self, salt: &[u8])-> Result> { + let rp_id = "cryptcrypt.cce"; + + let devs = ctap_hid_fido2::get_fidokey_devices(); + if devs.is_empty() { + return Err("FIDO authenticator missing".into()); + } else if devs.len() > 1 { + return Err("Exactly one FIDO authenticator supported, disconnect all but one.".into()); + } -/// Retrieves and combines password and optional key file hash into secret bytes. -/// -/// Reads and hashes the optional key file first (before prompting for password), -/// then prompts the user for a password and concatenates the password bytes with -/// the key file hash. Returns the combined data. -/// -/// # Arguments -/// - `keyfilepath`: Optional path to a key file to hash and append -/// - `verify_password`: If `true`, prompts user to repeat password and verifies they match. -/// If `false`, only prompts once. -/// -/// # Returns -/// - `Ok(pass_bytes)` containing concatenated password and key file hash -/// - `Err` if key file reading fails, password entry fails, or passwords don't match (when verify=true) -pub fn get_pass_bytes(keyfilepath: Option<&PathBuf>, verify_password: bool) -> Result> { - // read keyfile, if passed by user - // as that might fail, it is done before password is entered and then is available in memory - let mut hash= vec![]; - if let Some(keyfile) = keyfilepath { - hash.extend(read_and_hash_keyfile(keyfile)?); + let device = FidoKeyHidFactory::create_by_params( + &[devs[0].param.clone()], + &Cfg::init(), + )?; + + if !device.enable_info_param(&InfoParam::ExtensionsHmacSecret)? { + return Err("FIDO authenticator does not support the hmac-secret extension".into()); + } + + if Some(true) != device.enable_info_option(&InfoOption::AlwaysUv)? { + return Err("User verification on FIDO authenticator not enforced".into()); + } + + let mut pin = + if Some(true) == device.enable_info_option(&InfoOption::ClientPin)? { + Self::get_fido_pin_from_user()? + } else { + return Err("PIN on FIDO authenticator not enabled".into()) + }; + + let mut credential_public_key = None; + if self.credential_id_fido.is_none() { + // get credential ID + println!("Registering USB security key..."); + + let challenge = verifier::create_challenge(); + let ext = CredentialExtension::HmacSecret(Some(true)); + let make_credential_args = MakeCredentialArgsBuilder::new(rp_id, &challenge) + .pin(pin.expose_secret()) + .extensions(&[ext]) + .build(); + let attestation = device.make_credential_with_args(&make_credential_args)?; + + let find = attestation + .extensions + .iter() + .find(|&it| matches!(it, CredentialExtension::HmacSecret(Some(true)))); + if find.is_none() { + return Err("FIDO authenticator connection error".into()); + } + + let verify_result = verifier::verify_attestation(rp_id, &challenge, &attestation); + if !verify_result.is_success { + return Err("FIDO authenticator connection error".into()); + } + + self.credential_id_fido = Some(verify_result.credential_id); + credential_public_key = Some(verify_result.credential_public_key); + } + + println!("Requesting data from USB security key..."); + + // get hmac secret + let challenge = verifier::create_challenge(); + let ext = AssertionExtension::HmacSecret(Some(salt.try_into()?)); + + let get_assertion_args = GetAssertionArgsBuilder::new(rp_id, &challenge) + .pin(pin.expose_secret()) + .credential_id(self.credential_id_fido.as_ref().unwrap()) + .extensions(&[ext]) + .build(); + let assertions = device.get_assertion_with_args(&get_assertion_args)?; + pin.zeroize(); + + let assertion = assertions + .first() + .ok_or("FIDO authenticator connection error")?; + + let find = assertion + .extensions + .iter() + .find(|it| matches!(it, AssertionExtension::HmacSecret(_))); + let hmac_secret = + if let Some(AssertionExtension::HmacSecret(Some(hmac_sec))) = find { + SecretSlice::from(Vec::from(hmac_sec)) + } else { + return Err("FIDO authenticator connection error".into()) + }; + + if let Some(cred_pub_key) = credential_public_key + && !verifier::verify_assertion(rp_id, &cred_pub_key, &challenge, assertion) { + return Err("FIDO authenticator connection error".into()); + } + + println!("USB security key end"); + + Ok(hmac_secret) } - // get password string and append hash - let password = get_password_from_user(verify_password)?; - let pass_bytes = SecretSlice::from([password.expose_secret().as_bytes(), &hash].concat()); + /// Derives output key material (OKM) using HKDF-SHA256. + /// + /// HKDF (HMAC-based Key Derivation Function) expands a pseudo-random key (PRK) + /// into a derived key of specified length using provided info bytes. + /// + /// # Arguments + /// - `key`: The pseudo-random key (PRK) for HKDF with length 32. + /// - `salt`: Salt bytes + /// - `info`: Information bytes for HKDF expand phase. + /// + /// # Returns + /// - `Ok(okm)` output key material + /// - `Err` if PRK length is invalid or requested output length is invalid + fn key_derivation(key: &SecretSlice, salt: &[u8], info: &[u8]) -> Result> { + let hk = Hkdf::::new(Some(salt), key.expose_secret()); + let mut okm = SecretSlice::from(vec![0u8; KEY_SIZE]); + hk.expand(info, okm.expose_secret_mut()) + .map_err(|e| format!("Invalid length for HKDF expand: {:?}", e))?; + Ok(okm) + } - Ok(pass_bytes) -} + /// Creates the encryption ciphers used for the file header. + /// + /// Prompts for the password, derives a key with Argon2 using `salt_pw`, + /// then derives separate XChaCha20-Poly1305 and AES-256-GCM-SIV keys + /// with HKDF-SHA256 using `salt_enc`. + /// + /// # Arguments + /// - `salt_pw`: Salt used for Argon2 password hashing + /// - `salt_enc`: Salt used for HKDF key derivation + /// + /// # Returns + /// - `Ok((cipher_cha, cipher_aes))` containing the header encryption ciphers + /// - `Err` if password input, password hashing, key derivation, or cipher + /// initialization fails + pub fn get_password_and_create_header_ciphers(&mut self, salt_pw: &[u8], salt_enc: &[u8]) -> Result<(XChaCha20Poly1305, Aes256GcmSiv)> { + self.get_password_from_user()?; + + let mut key = SecretSlice::from(vec![0u8; KEY_SIZE]); + let argon = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::default(), self.argon2_params.clone()); + argon.hash_password_into(&self.password.decrypt()?, salt_pw, key.expose_secret_mut()) + .map_err(|_e| "Failed to hash password")?; + + let key_cha = Self::key_derivation(&key, salt_enc, "header-xchacha20poly1305".as_bytes())?; + let key_aes = Self::key_derivation(&key, salt_enc, "-header-aes-256-gcm-siv-".as_bytes())?; + key.zeroize(); + + let cipher_cha = XChaCha20Poly1305::new_from_slice(key_cha.expose_secret())?; + let cipher_aes = Aes256GcmSiv::new_from_slice(key_aes.expose_secret())?; + + Ok((cipher_cha, cipher_aes)) + } -/// Derives output key material (OKM) using HKDF-SHA256. -/// -/// HKDF (HMAC-based Key Derivation Function) expands a pseudo-random key (PRK) -/// into a derived key of specified length using provided info bytes. -/// -/// # Arguments -/// - `key`: The pseudo-random key (PRK) for HKDF with length 32. -/// - `salt`: Salt bytes -/// - `info`: Information bytes for HKDF expand phase. -/// -/// # Returns -/// - `Ok(okm)` output key material -/// - `Err` if PRK length is invalid or requested output length is invalid -pub fn key_derivation(key: &SecretSlice, salt: &[u8], info: &[u8]) -> Result> { - let hk = Hkdf::::new(Some(salt), key.expose_secret()); - let mut okm = SecretSlice::from(vec![0u8; KEY_SIZE]); - hk.expand(info, okm.expose_secret_mut()) - .map_err(|e| format!("Invalid length for HKDF expand: {:?}", e))?; - Ok(okm) + /// Creates the encryption ciphers used for the file body. + /// + /// Combines the password with any supplied key-file hash and FIDO2 + /// hmac-secret, derives a key with Argon2 using `salt_pw`, then derives + /// separate XChaCha20-Poly1305 and AES-256-GCM-SIV keys with HKDF-SHA256 + /// using `salt_enc`. + /// + /// # Arguments + /// - `salt_pw`: Salt used for Argon2 password hashing + /// - `salt_enc`: Salt used for HKDF key derivation + /// - `keyfilepath`: Optional path to a key file; its hash is used + /// as an additional key input + /// - `salt_fido`: Optional salt used to request an hmac-secret from the + /// FIDO2 authenticator + /// + /// # Returns + /// - `Ok((cipher_cha, cipher_aes))` containing the body encryption ciphers + /// - `Err` if key-file access, FIDO2 interaction, password hashing, key + /// derivation, or cipher initialization fails + pub fn create_body_ciphers(&mut self, salt_pw: &[u8], salt_enc: &[u8], keyfilepath: Option<&PathBuf>, salt_fido: Option<&[u8]>) -> Result<(XChaCha20Poly1305, Aes256GcmSiv)> { + let mut pwd = Zeroizing::new( + Vec::with_capacity(64 + 32 + self.password.cleartext_len) + ); + + if let Some(path) = keyfilepath { + pwd.extend( + Self::read_and_hash_keyfile(path)? + ); + } + + if let Some(salt) = salt_fido { + pwd.extend( + self.usb_fido2_hmac_secret(salt)?.expose_secret() + ); + } + + pwd.extend(self.password.decrypt()?); + + let mut key = SecretSlice::from(vec![0u8; KEY_SIZE]); + let argon = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::default(), self.argon2_params.clone()); + argon.hash_password_into(&pwd, salt_pw, key.expose_secret_mut()).map_err(|_e| "Failed to hash password")?; + pwd.zeroize(); + self.password.zeroize(); + + let key_cha = Self::key_derivation(&key, salt_enc, "body-xchacha20poly1305".as_bytes())?; + let key_aes = Self::key_derivation(&key, salt_enc, "-body-aes-256-gcm-siv-".as_bytes())?; + key.zeroize(); + + let cipher_cha = XChaCha20Poly1305::new_from_slice(key_cha.expose_secret())?; + let cipher_aes = Aes256GcmSiv::new_from_slice(key_aes.expose_secret())?; + + Ok((cipher_cha, cipher_aes)) + } } @@ -140,16 +412,36 @@ pub fn key_derivation(key: &SecretSlice, salt: &[u8], info: &[u8]) -> Result mod tests { use super::*; use std::fs; + use aes_gcm_siv::aead::Aead; use rand::Rng; - use crate::SALT_SIZE; + use crate::{AES_NONCE_SIZE, CHA_NONCE_SIZE, SALT_SIZE}; + + #[test] + fn test_crypt_memory() { + let plaintext = b"sensitive test data"; + let mut memory = CryptMemory::new(plaintext).unwrap(); + + assert_eq!(memory.cleartext_len, plaintext.len()); + assert_ne!(memory.data, plaintext); + assert_eq!(memory.decrypt().unwrap(), plaintext); + + let mut empty_memory = CryptMemory::new(&[]).unwrap(); + assert_eq!(empty_memory.cleartext_len, 0); + assert!(empty_memory.decrypt().unwrap().is_empty()); + + memory.data[0] ^= 1; + assert!(memory.decrypt().is_err()); + } #[test] fn test_get_password_from_user() { - let pw = get_password_from_user(false).unwrap(); - assert_eq!("abc123test", pw.expose_secret()); + let mut kb = KeyBuilder::new(false); + kb.get_password_from_user().unwrap(); + assert_eq!("abc123test".as_bytes(), kb.password.decrypt().unwrap()); - let pw = get_password_from_user(true).unwrap(); - assert_eq!("abc123test", pw.expose_secret()); + let mut kb = KeyBuilder::new(true); + kb.get_password_from_user().unwrap(); + assert_eq!("abc123test".as_bytes(), kb.password.decrypt().unwrap()); } #[test] @@ -161,24 +453,24 @@ mod tests { fs::write(&filepath_kf, data_kf).unwrap(); // repeated read should return the same hash which should not be all zeros - let hash1 = read_and_hash_keyfile(&filepath_kf).unwrap(); - let hash2 = read_and_hash_keyfile(&filepath_kf).unwrap(); + let hash1 = KeyBuilder::read_and_hash_keyfile(&filepath_kf).unwrap(); + let hash2 = KeyBuilder::read_and_hash_keyfile(&filepath_kf).unwrap(); assert_eq!(hash1, hash2); assert_ne!(hash1, [0u8; 64]); // empty file let data_kf = []; fs::write(&filepath_kf, data_kf).unwrap(); - let hash1 = read_and_hash_keyfile(&filepath_kf).unwrap(); - let hash2 = read_and_hash_keyfile(&filepath_kf).unwrap(); + let hash1 = KeyBuilder::read_and_hash_keyfile(&filepath_kf).unwrap(); + let hash2 = KeyBuilder::read_and_hash_keyfile(&filepath_kf).unwrap(); assert_eq!(hash1, hash2); assert_ne!(hash1, [0u8; 64]); // big file let data_kf = vec![0xa5; MAX_KEYFILE_CHUNKS * CHUNK_SIZE]; fs::write(&filepath_kf, data_kf).unwrap(); - let hash1 = read_and_hash_keyfile(&filepath_kf).unwrap(); - let hash2 = read_and_hash_keyfile(&filepath_kf).unwrap(); + let hash1 = KeyBuilder::read_and_hash_keyfile(&filepath_kf).unwrap(); + let hash2 = KeyBuilder::read_and_hash_keyfile(&filepath_kf).unwrap(); assert_eq!(hash1, hash2); assert_ne!(hash1, [0u8; 64]); @@ -186,34 +478,13 @@ mod tests { let mut data_kf = vec![0; CHUNK_SIZE + 10]; rand::rng().fill_bytes(&mut data_kf); fs::write(&filepath_kf, data_kf).unwrap(); - let hash1 = read_and_hash_keyfile(&filepath_kf).unwrap(); - let hash2 = read_and_hash_keyfile(&filepath_kf).unwrap(); + let hash1 = KeyBuilder::read_and_hash_keyfile(&filepath_kf).unwrap(); + let hash2 = KeyBuilder::read_and_hash_keyfile(&filepath_kf).unwrap(); assert_eq!(hash1, hash2); assert_ne!(hash1, [0u8; 64]); // key file does not exist - assert!(read_and_hash_keyfile(&PathBuf::from("test_miss")).is_err()); - - // cleanup - let _ = fs::remove_file(&filepath_kf); - } - - #[test] - fn test_get_pass_bytes() { - // no key file - let pass = get_pass_bytes(None, false).unwrap(); - assert_eq!("abc123test".as_bytes(), pass.expose_secret()); - - // with key file - let filepath_kf = PathBuf::from("test_gpb.bin"); - let data_kf = [0xa5; 50]; - fs::write(&filepath_kf, data_kf).unwrap(); - let pass = get_pass_bytes(Some(&filepath_kf), false).unwrap(); - assert!(pass.expose_secret().starts_with("abc123test".as_bytes())); - assert_eq!(pass.expose_secret().len(), "abc123test".len() + 64); - - // key file does not exist - assert!(get_pass_bytes(Some(&PathBuf::from("test_miss")), false).is_err()); + assert!(KeyBuilder::read_and_hash_keyfile(&PathBuf::from("test_miss")).is_err()); // cleanup let _ = fs::remove_file(&filepath_kf); @@ -227,8 +498,8 @@ mod tests { let info = [0xa; 12]; // output shouldn't change if called again - let okm1 = key_derivation(&key_sec, &salt, &info).unwrap(); - let okm2 = key_derivation(&key_sec, &salt, &info).unwrap(); + let okm1 = KeyBuilder::key_derivation(&key_sec, &salt, &info).unwrap(); + let okm2 = KeyBuilder::key_derivation(&key_sec, &salt, &info).unwrap(); assert_eq!(okm1.expose_secret(), okm2.expose_secret()); assert_eq!(okm1.expose_secret().len(), KEY_SIZE); assert_ne!(okm1.expose_secret(), [0; KEY_SIZE]); @@ -236,25 +507,118 @@ mod tests { // different output on different info input let info = [0xb; 12]; - let okm3 = key_derivation(&key_sec, &salt, &info).unwrap(); + let okm3 = KeyBuilder::key_derivation(&key_sec, &salt, &info).unwrap(); assert_ne!(okm1.expose_secret(), okm3.expose_secret()); // different output on different info input let mut salt4 = salt; salt4[0] ^= 0xFF; - let okm4 = key_derivation(&key_sec, &salt4, &info).unwrap(); + let okm4 = KeyBuilder::key_derivation(&key_sec, &salt4, &info).unwrap(); assert_ne!(okm3.expose_secret(), okm4.expose_secret()); // different output on different key input let mut key4 = key; key4[0] ^= 0x01; - let okm5 = key_derivation(&SecretSlice::from(key4.to_vec()), &salt4, &info).unwrap(); + let okm5 = KeyBuilder::key_derivation(&SecretSlice::from(key4.to_vec()), &salt4, &info).unwrap(); assert_ne!(okm4.expose_secret(), okm5.expose_secret()); // Even empty salt, should produce a valid non-zero key let salt6= []; - let okm6 = key_derivation(&key_sec, &salt6, &info).unwrap(); + let okm6 = KeyBuilder::key_derivation(&key_sec, &salt6, &info).unwrap(); assert_ne!(okm6.expose_secret(), [0u8; KEY_SIZE]); assert_ne!(okm1.expose_secret(), okm6.expose_secret()); } + + #[test] + fn test_get_password_and_create_header_ciphers() { + let salt_pw = [1u8; SALT_SIZE]; + let salt_enc = [2u8; SALT_SIZE]; + + let mut kb = KeyBuilder::new(false); + let (cipher_cha, cipher_aes) = kb + .get_password_and_create_header_ciphers(&salt_pw, &salt_enc).unwrap(); + assert_eq!(kb.password.decrypt().unwrap(), "abc123test".as_bytes()); + + // both ciphers must be able to encrypt/decrypt a message + let nonce_cha = [0u8; CHA_NONCE_SIZE]; + let nonce_aes = [0u8; AES_NONCE_SIZE]; + let plaintext = b"header-data"; + + let ct_cha = cipher_cha.encrypt(&nonce_cha.into(), plaintext.as_ref()).unwrap(); + let pt_cha = cipher_cha.decrypt(&nonce_cha.into(), ct_cha.as_ref()).unwrap(); + assert_eq!(pt_cha, plaintext); + + let ct_aes = cipher_aes.encrypt(&nonce_aes.into(), plaintext.as_ref()).unwrap(); + let pt_aes = cipher_aes.decrypt(&nonce_aes.into(), ct_aes.as_ref()).unwrap(); + assert_eq!(pt_aes, plaintext); + + // changing the salt changes the derived key / ciphertext + let mut kb2 = KeyBuilder::new(false); + let (cipher_cha2, cipher_aes2) = kb2 + .get_password_and_create_header_ciphers(&[9u8; SALT_SIZE], &salt_enc) .unwrap(); + + let ct_cha2 = cipher_cha2.encrypt(&nonce_cha.into(), plaintext.as_ref()).unwrap(); + let ct_aes2 = cipher_aes2.encrypt(&nonce_aes.into(), plaintext.as_ref()).unwrap(); + assert_ne!(ct_cha, ct_cha2); + assert_ne!(ct_aes, ct_aes2); + } + + #[test] + fn test_create_body_ciphers() { + let salt_pw = [1u8; SALT_SIZE]; + let salt_enc = [2u8; SALT_SIZE]; + + let mut kb = KeyBuilder::new(false); + let _ = kb.get_password_and_create_header_ciphers(&salt_pw, &salt_enc).unwrap(); + + let (cipher_cha, cipher_aes) = kb + .create_body_ciphers(&salt_pw, &salt_enc, None, None).unwrap(); + + // both ciphers must be able to encrypt/decrypt a message + let nonce_cha = [0u8; CHA_NONCE_SIZE]; + let nonce_aes = [0u8; AES_NONCE_SIZE]; + let plaintext = b"body-data"; + + let ct_cha = cipher_cha.encrypt(&nonce_cha.into(), plaintext.as_ref()).unwrap(); + let pt_cha = cipher_cha.decrypt(&nonce_cha.into(), ct_cha.as_ref()).unwrap(); + assert_eq!(pt_cha, plaintext); + + let ct_aes = cipher_aes.encrypt(&nonce_aes.into(), plaintext.as_ref()).unwrap(); + let pt_aes = cipher_aes.decrypt(&nonce_aes.into(), ct_aes.as_ref()).unwrap(); + assert_eq!(pt_aes, plaintext); + + // changing the salt changes the derived key / ciphertext + let mut kb2 = KeyBuilder::new(false); + let _ = kb2.get_password_and_create_header_ciphers(&[9u8; SALT_SIZE], &salt_enc) .unwrap(); + let (cipher_cha2, cipher_aes2) = kb2 + .create_body_ciphers(&[9u8; SALT_SIZE], &salt_enc, None, None).unwrap(); + + let ct_cha2 = cipher_cha2.encrypt(&nonce_cha.into(), plaintext.as_ref()).unwrap(); + let ct_aes2 = cipher_aes2.encrypt(&nonce_aes.into(), plaintext.as_ref()).unwrap(); + assert_ne!(ct_cha, ct_cha2); + assert_ne!(ct_aes, ct_aes2); + + // with keyfile + let keyfile = PathBuf::from("test_hdr_body_keyfile.bin"); + fs::write(&keyfile, vec![0xA5; 128]).unwrap(); + + let mut kb = KeyBuilder::new(false); + let _ = kb.get_password_and_create_header_ciphers(&salt_pw, &salt_enc).unwrap(); + let (cipher_cha3, cipher_aes3) = kb + .create_body_ciphers(&salt_pw, &salt_enc, Some(&keyfile), None).unwrap(); + + let ct_cha3 = cipher_cha3.encrypt(&nonce_cha.into(), plaintext.as_ref()).unwrap(); + let pt_cha = cipher_cha3.decrypt(&nonce_cha.into(), ct_cha3.as_ref()).unwrap(); + assert_eq!(pt_cha, plaintext); + + let ct_aes3 = cipher_aes3.encrypt(&nonce_aes.into(), plaintext.as_ref()).unwrap(); + let pt_aes = cipher_aes3.decrypt(&nonce_aes.into(), ct_aes3.as_ref()).unwrap(); + assert_eq!(pt_aes, plaintext); + + // ciphertexts should change with keyfile + assert_ne!(ct_cha, ct_cha3); + assert_ne!(ct_aes, ct_aes3); + + let _ = fs::remove_file(&keyfile); + } } \ No newline at end of file diff --git a/src/common_io.rs b/src/common_io.rs index 908460f..6d0a39c 100644 --- a/src/common_io.rs +++ b/src/common_io.rs @@ -71,7 +71,7 @@ impl ReadInput { if f_in_header_size != 0 && f_in_total_size < f_in_header_size + (CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE) as u64 { - return Err("File cannot be decoded".into()); + return Err("File cannot be decrypted".into()); } // header was already read, therefore remaining file size must be reduced by the header's size @@ -256,34 +256,46 @@ impl WriteFiles for WriteOutput { } } +/// Shared configuration for the stream-oriented cryptographic pipeline. +/// +/// The same config is passed to all worker threads spawned by `CryptIo::io_chunks`. +pub struct CryptConfig { + /// Cipher used for the ChaCha20-Poly1305 layer. + pub cipher_cha: XChaCha20Poly1305, + /// Cipher used for the AES-256-GCM-SIV layer. + pub cipher_aes: Aes256GcmSiv, + /// Whether the payload is compressed before encryption and decompressed after decryption. + pub compress: bool, + /// Additional per-file associated data included in the authenticated chunk metadata. + pub more_associated_data: Vec, +} /// Struct for common cryptographic I/O operations pub struct CryptIo; impl CryptIo { - /// Performs cryptographic I/O operations and optional compression on a file using chunked processing with multithreading. + /// Processes a stream of input chunks through a parallel crypto pipeline and writes the ordered output. /// - /// Reads input file in chunks, applies optional compression and two sequential cryptographic functions to each chunk - /// using parallelism by threads and writes the results. + /// `read_input` yields chunks as `(Vec, bool)`, where the boolean marks the final chunk. + /// Each chunk is paired with its sequence number and sent into the channel-based worker pipeline + /// created by `crypt_fn`. The workers are expected to return output chunks in `(Vec, u32)` + /// order, and the writer thread reorders them before handing them to `write_output`. /// /// # Arguments - /// - `cipher_cha`: cipher struct for XChaCha20-Poly1305 - /// - `cipher_aes`: cipher struct for AES-256-GCM-SIV - /// - `compress`: Compress data before encryption, decompress after decryption - /// - `crypt_fn`: Cryptographic function processing compression, XChaCha20-Poly1305, AES-256-GCM-SIV + /// - `crypt_cfg`: Shared cryptographic state and flags for the active pipeline. + /// - `crypt_fn`: Function that creates the worker threads; it receives the input channel, + /// output channel and CPU count and returns the join handles for the worker threads. + /// - `read_input`: Source providing chunked input data and thread join handling. + /// - `write_output`: Sink that writes ordered processed chunks and any trailing output. /// /// # Returns - /// - `Ok(())` on successful completion - /// - `Err` if file I/O fails, channels fail or cryptographic functions fail + /// - `Ok(())` on successful completion. + /// - `Err` if an I/O error, channel error or worker panic occurs. #[allow(clippy::type_complexity)] pub fn io_chunks( - cipher_cha: &XChaCha20Poly1305, - cipher_aes: &Aes256GcmSiv, - compress: bool, + crypt_cfg: &CryptConfig, crypt_fn: fn( - &XChaCha20Poly1305, - &Aes256GcmSiv, - bool, + &CryptConfig, Receiver<(Vec, u32, bool)>, Sender<(Vec, u32)>, usize) -> Vec>>, @@ -298,7 +310,7 @@ impl CryptIo { let (tx_out, rx_out) = bounded(cpu_count); // compress (optionally) and encrypt/decrypt in threads - let crypt_handles = crypt_fn(cipher_cha, cipher_aes, compress, rx_in, tx_out, cpu_count); + let crypt_handles = crypt_fn(crypt_cfg, rx_in, tx_out, cpu_count); // write output file(s) in a thread // chunks have to be ordered first, as durations of parallel threads varies diff --git a/src/decryption.rs b/src/decryption.rs index 0fba26c..5d03b83 100644 --- a/src/decryption.rs +++ b/src/decryption.rs @@ -4,18 +4,17 @@ use std::path::PathBuf; use std::collections::HashMap; use std::fs; use std::env; -use argon2::Argon2; use chacha20poly1305::{XChaCha20Poly1305, XNonce}; -use aes_gcm_siv::{aead::KeyInit, Aes256GcmSiv, AeadInOut, Nonce}; -use secrecy::{ExposeSecret, ExposeSecretMut, SecretSlice}; +use aes_gcm_siv::{Aes256GcmSiv, AeadInOut, Nonce}; use bzip2::read::BzDecoder; use crossbeam_channel::{bounded, Sender, Receiver}; -use crate::{Result, KEY_SIZE, CHA_NONCE_SIZE, AES_NONCE_SIZE, CHUNK_SIZE, COMPRESS_LENGTH_SIZE, ENCRYPTED_FILE_EXT, +use crate::{Result, CHA_NONCE_SIZE, AES_NONCE_SIZE, CHUNK_SIZE, FILE_ID_SIZE, COMPRESS_LENGTH_SIZE, ENCRYPTED_FILE_EXT, SPLIT_ENC_FILE_EXT, CHA_TAG_SIZE, AES_TAG_SIZE, HEADER_SIZE, SALT_SIZE, FILE_FORMAT_VERSION}; -use crate::common::{get_pass_bytes, key_derivation}; -use crate::common_io::{CryptIo, ReadInput, WriteFiles, WriteOutput}; +use crate::common::KeyBuilder; +use crate::common_io::{CryptIo, CryptConfig, ReadInput, WriteFiles, WriteOutput}; use crate::archive::ArchiveWrite; +use crate::encryption::EncHeaderParams; /// Handles file decryption operations using dual-layer decryption and decompression. @@ -24,51 +23,6 @@ use crate::archive::ArchiveWrite; pub struct Decryption; impl Decryption { - /// Hashes a user-provided password using Argon2 with supplied salt and optional key file. - /// - /// Derives a cryptographic key from the password using the provided salt, the optional key file, - /// and Argon2id algorithm, allowing recovery of the original key used for encryption. - /// - /// # Arguments - /// - `salt_pw`: Salt bytes to use for hashing - /// - `keyfilepath`: Optional path to an additional key file - /// - /// # Returns - /// - `Ok(key)` containing the derived key - /// - `Err` if password hashing or getting password/key file fails - fn hash_password(salt_pw: &[u8], keyfilepath: Option<&PathBuf>) -> Result> { - let pass = get_pass_bytes(keyfilepath, false)?; - - let mut key = SecretSlice::from(vec![0u8; KEY_SIZE]); - Argon2::default().hash_password_into(pass.expose_secret(), salt_pw, key.expose_secret_mut()) - .map_err(|e| format!("Failed to hash password: {:?}", e))?; - - Ok(key) - } - - /// Derive ChaCha20 and AES keys from a master key and salts. - /// - /// Expands the provided master `key` with HKDF-SHA256 using `salt_cha` and - /// `salt_aes` to produce two independent 32‑byte keys. - /// - /// The function returns the derived keys wrapped in `SecretSlice` so - /// callers keep key material in secure containers. - /// - /// # Arguments - /// - `salt_cha`: salt for the ChaCha key derivation (expected length: `SALT_SIZE`) - /// - `salt_aes`: salt for the AES key derivation (expected length: `SALT_SIZE`) - /// - `key`: master secret material to expand (type: `SecretSlice`) - /// - /// # Returns - /// - `Ok((key_cha, key_aes))` — tuple of derived keys (`SecretSlice`) each `KEY_SIZE` bytes long - /// - `Err` if HKDF expansion or underlying operations fail - fn derive_keys(salt_cha: &[u8], salt_aes: &[u8], key: &SecretSlice) -> Result<(SecretSlice, SecretSlice)> { - let key_cha = key_derivation(key, salt_cha, "xchacha20poly1305".as_bytes())?; - let key_aes = key_derivation(key, salt_aes, "-aes-256-gcm-siv-".as_bytes())?; - - Ok((key_cha, key_aes)) - } - /// Decrypts a buffer encrypted with XChaCha20-Poly1305 using the per‑chunk nonce modification. /// /// Extracts the nonce from the end of the buffer and decrypts the remainder. @@ -76,17 +30,23 @@ impl Decryption { /// The function reconstructs the modified nonce by XOR'ing: /// - `nonce[0]` with `final_chunk`, and /// - `nonce[1..]` with the little‑endian bytes of `chunk_count` (applied starting at index 1). + /// + /// The authenticated `associated_data` for a chunk is built from the file format + /// version, a characterizing string, and a caller-provided array. + /// This value must match the metadata used during encryption or authentication will fail. /// /// # Arguments /// - `cipher`: XChaCha20-Poly1305 cipher struct initialized with key. /// - `buf_inout`: data-in: ciphertext (+ authentication tag) + nonce; data-out: decrypted data. /// - `chunk_count`: zero‑based chunk index; must match the value used during encryption. /// - `final_chunk`: `true` if this is the last chunk; must match the value used during encryption. - /// + /// - `more_associated_data`: Additional per-file metadata folded into the + /// authenticated `associated_data` for this chunk; must match the value used during encryption. + /// /// # Returns /// - `Ok()` on successful decryption /// - `Err` if decryption fails or authentication tag verification fails - pub fn cha_decrypt_buffer(cipher: &XChaCha20Poly1305, buf_inout: &mut Vec, chunk_count: u32, final_chunk: bool) -> Result<()> { + pub fn cha_decrypt_buffer(cipher: &XChaCha20Poly1305, buf_inout: &mut Vec, chunk_count: u32, final_chunk: bool, more_associated_data: &[u8]) -> Result<()> { let nonce_pos = buf_inout.len() - CHA_NONCE_SIZE; let mut nonce = XNonce::try_from(&buf_inout[nonce_pos..])?; @@ -96,7 +56,12 @@ impl Decryption { nonce[i+1] ^= ccb; } buf_inout.truncate(nonce_pos); - cipher.decrypt_in_place(&nonce, &[], buf_inout) + + let mut associated_data = vec![FILE_FORMAT_VERSION]; + associated_data.extend("Cha".as_bytes()); + associated_data.extend(more_associated_data); + + cipher.decrypt_in_place(&nonce, &associated_data, buf_inout) .map_err(|_e| "Failed to decrypt data!")?; // for better user info Ok(()) } @@ -106,18 +71,34 @@ impl Decryption { /// Extracts the nonce from the end of the buffer and decrypts the remainder. /// Verifies authentication tag during decryption. /// + /// The authenticated `associated_data` for an chunk is built from the file format + /// version, characterizing string, and a caller-provided array, the little-endian bytes + /// of `chunk_count`, and the `final_chunk` flag. This value must match the metadata used + /// during encryption or authentication will fail. + /// /// # Arguments /// - `cipher`: Aes256GcmSiv cipher struct initialized with key /// - `buf_inout`: data-in: ciphertext (+ authentication tag) + nonce; data-out: decrypted data - /// + /// - `chunk_count`: zero‑based chunk index; must match the value used during encryption. + /// - `final_chunk`: `true` if this is the last chunk; must match the value used during encryption. + /// - `more_associated_data`: Additional per-file metadata folded into the + /// authenticated `associated_data` for this chunk; must match the value used during encryption. + /// /// # Returns /// - `Ok()` on successful decryption /// - `Err` if decryption fails or authentication tag verification fails - pub fn aes_decrypt_buffer(cipher: &Aes256GcmSiv, buf_inout: &mut Vec) -> Result<()> { + pub fn aes_decrypt_buffer(cipher: &Aes256GcmSiv, buf_inout: &mut Vec, chunk_count: u32, final_chunk: bool, more_associated_data: &[u8]) -> Result<()> { let nonce_pos = buf_inout.len() - AES_NONCE_SIZE; let nonce = Nonce::try_from(&buf_inout[nonce_pos..])?; buf_inout.truncate(nonce_pos); - cipher.decrypt_in_place(&nonce, &[], buf_inout) + + let mut associated_data = vec![FILE_FORMAT_VERSION]; + associated_data.extend("Aes".as_bytes()); + associated_data.extend(more_associated_data); + associated_data.extend(chunk_count.to_le_bytes()); + associated_data.push(u8::from(final_chunk)); + + cipher.decrypt_in_place(&nonce, &associated_data, buf_inout) .map_err(|_e| "Failed to decrypt data!")?; // for better user info Ok(()) } @@ -205,35 +186,38 @@ impl Decryption { Ok(()) } - /// Creates the multithreaded decryption pipeline and returns thread handles. + /// Creates the multithreaded decryption pipeline and returns all worker handles. /// - /// Spawns worker threads to reverse the encryption pipeline and produce plaintext chunks - /// for writing. Behavior depends on `decompress`: - /// - Always spawns `cpu_count` decryption threads: each reads `(Vec, chunk_count, final_flag)` - /// from `rx_in`, performs AES-256-GCM-SIV decryption, then XChaCha20-Poly1305 decryption - /// (reconstructing per-chunk nonce using `chunk_count` and `final_flag`), and sends - /// `(Vec, chunk_count)`. - /// - If `decompress == true`: - /// - Spawns a single re-segmentation thread that reads ordered decrypted inner payloads, - /// stitches and parses length-prefixed compressed entries, and forwards `(Vec, u32)` - /// to the decompression channel. - /// - Spawns `cpu_count` decompression threads that read `(Vec, chunk_count)` from the - /// re-segmentation output, decompress each entry, and send `(Vec, chunk_count)` to `tx_out`. + /// Incoming encrypted chunks are read from `rx_in` and processed in reverse order to + /// the encryption pipeline. The function always starts decryption workers first. When + /// compression is enabled, the decrypted inner payloads are reassembled, + /// length-prefixed compressed fragments are extracted, and the recovered entries are + /// decompressed before they are forwarded to `tx_out`. + /// + /// The exact behavior is: + /// - allocate `cpu_count` decryption workers when compression is off, + /// or `max(1, cpu_count - c_thread_cnt)` workers when compression is on + /// - each decryption worker reads `(Vec, u32, bool)` from `rx_in`, runs + /// AES-256-GCM-SIV and XChaCha20-Poly1305 decryption in sequence, and sends the + /// decrypted chunk as `(Vec, u32)` to either `tx_out` directly or to an internal + /// resegmentation channel if compression is active + /// - when compression is enabled, a single resegmentation thread consumes the + /// decrypted chunks, concatenates them, reconstructs the length-prefixed compressed + /// entries, and sends each extracted payload as `(Vec, u32)` onward + /// - decompression workers then call `decompress_buffer` and emit the final plaintext + /// chunks `(Vec, u32)` to `tx_out` /// /// # Arguments - /// - `cipher_cha`: cipher struct for XChaCha20-Poly1305 decryption - /// - `cipher_aes`: cipher struct for AES-256-GCM-SIV decryption - /// - `decompress`: enable resegment + decompression stages when true. - /// - `rx_in`: receiver for encrypted input chunks `(data, chunk_count, final_flag)`. - /// - `tx_out`: sender for final plaintext output `(plaintext_chunk, chunk_count)`. - /// - `cpu_count`: number of worker threads to spawn. + /// - `crypt_cfg`: Shared configuration containing the ciphers and the compression flag. + /// - `rx_in`: Receiver of encrypted chunks as `(Vec, chunk_count, final_chunk)`. + /// - `tx_out`: Sender for final plaintext chunks as `(Vec, chunk_count)`. + /// - `cpu_count`: Number of logical CPU cores used to size the worker pool. /// /// # Returns - /// - `Vec>>` — handles for all spawned threads. + /// - `Vec>>` containing the decryption, + /// resegmentation, and decompression worker handles. pub fn decrypt_pipe( - cipher_cha: &XChaCha20Poly1305, - cipher_aes: &Aes256GcmSiv, - decompress: bool, + crypt_cfg: &CryptConfig, rx_in: Receiver<(Vec, u32, bool)>, tx_out: Sender<(Vec, u32)>, cpu_count: usize @@ -242,7 +226,7 @@ impl Decryption { // number of threads used for decryption and decompression let c_thread_cnt; let e_thread_cnt; - if decompress { + if crypt_cfg.compress { c_thread_cnt = 1.max(cpu_count * 8 / 10); e_thread_cnt = 1.max(cpu_count - c_thread_cnt); } else { @@ -256,22 +240,25 @@ impl Decryption { // decryption threads for _ in 0..e_thread_cnt { - let cipher_cha= cipher_cha.clone(); - let cipher_aes = cipher_aes.clone(); + let cipher_cha= crypt_cfg.cipher_cha.clone(); + let cipher_aes = crypt_cfg.cipher_aes.clone(); + let more_associated_data = crypt_cfg.more_associated_data.clone(); let rx_in = rx_in.clone(); - let tx_e = if decompress { tx_e.clone() } else { tx_out.clone() }; + let tx_e = if crypt_cfg.compress { tx_e.clone() } else { tx_out.clone() }; thread_handles.push(thread::spawn( move || -> std::result::Result<(), String> { for (mut buf_inout, chunk_count, final_chunk) in rx_in { - Self::aes_decrypt_buffer(&cipher_aes, &mut buf_inout).map_err(|e| e.to_string())?; - Self::cha_decrypt_buffer(&cipher_cha, &mut buf_inout, chunk_count, final_chunk).map_err(|e| e.to_string())?; + Self::aes_decrypt_buffer(&cipher_aes, &mut buf_inout, chunk_count, final_chunk, &more_associated_data) + .map_err(|e| e.to_string())?; + Self::cha_decrypt_buffer(&cipher_cha, &mut buf_inout, chunk_count, final_chunk, &more_associated_data) + .map_err(|e| e.to_string())?; if tx_e.send((buf_inout, chunk_count)).is_err() { break } } Ok(()) })); } - if decompress { + if crypt_cfg.compress { let (tx_c, rx_c) = bounded(cpu_count * 2); // re-segmentation thread @@ -302,86 +289,136 @@ impl Decryption { thread_handles } - /// Gets unencrypted data of header, i.e. the 3 salt values + /// Gets unencrypted data of header /// /// # Argument: /// - `header`: header bytes /// /// # Returns: - /// - `(salt_pw, salt_cha, salt_aes)`: salts for password, chacha key, aes key - fn get_unencrypted_header_items(header: &[u8]) -> (&[u8], &[u8], &[u8]) { - let salt_pw = &header[..SALT_SIZE]; - let salt_cha = &header[SALT_SIZE..(2 * SALT_SIZE)]; - let salt_aes = &header[(2 * SALT_SIZE)..(3 * SALT_SIZE)]; - - (salt_pw, salt_cha, salt_aes) + /// - `(salt_header_pw, salt_header_enc)`: salts for password, cha/aes keys + pub fn get_unencrypted_header_items(header_inout: &mut Vec) -> (Vec, Vec) { + let salt_header_pw = header_inout.drain(..SALT_SIZE).collect(); + let salt_header_enc = header_inout.drain(..SALT_SIZE).collect(); + (salt_header_pw, salt_header_enc) } /// Gets encrypted data of header /// /// # Argument: - /// - `header`: header bytes + /// - `enc_header`: encrypted header bytes (without unencrypted bytes) /// - `cipher_cha`: cipher struct for chacha decryption /// - `cipher_aes`: cipher struct for aes decryption /// /// # Returns: - /// - `Ok((file_format_version, compress, archive))` contains on success: version of file format, compression status, whether it's an archive - fn get_encrypted_header_items(header: &[u8], cipher_cha: &XChaCha20Poly1305, cipher_aes: &Aes256GcmSiv) -> Result<(u8, bool, bool)> { - let mut enc_head = Vec::from(&header[(3 * SALT_SIZE)..HEADER_SIZE]); + /// - `Ok((file_format_version, enc_header_params))` contains on success: + /// version of file format, struct of encrypted header parameters + pub fn get_encrypted_header_items<'a>(enc_header: &'a mut Vec, cipher_cha: &XChaCha20Poly1305, cipher_aes: &Aes256GcmSiv) -> Result<(u8, EncHeaderParams<'a>)> { + Self::aes_decrypt_buffer(cipher_aes, enc_header, u32::MAX, true, "header_aes".as_bytes())?; + Self::cha_decrypt_buffer(cipher_cha, enc_header, 0, false, "header_cha".as_bytes())?; - Self::aes_decrypt_buffer(cipher_aes, &mut enc_head)?; - Self::cha_decrypt_buffer(cipher_cha, &mut enc_head, u32::MAX, false)?; + // evaluate data + let file_format_version = enc_header[0]; + let file_format = enc_header[1]; - // evaluate data, ignore random bytes - let file_format_version = enc_head[1]; - let file_format = enc_head[3]; let compress = (file_format & 0x01) != 0; let archive = (file_format & 0x02) != 0; + let keyfile = (file_format & 0x04) != 0; + let usb_key = (file_format & 0x08) != 0; + + let mut s = 2; + let mut e = s + SALT_SIZE; + let salt_body_pw = &enc_header[s..e]; + s = e; e += SALT_SIZE; + let salt_body_enc = &enc_header[s..e]; + s = e; e += FILE_ID_SIZE; + let file_id = &enc_header[s..e]; + + let mut salt_body_fido = None; + let mut credential_id_fido = None; + if usb_key { + s = e; e += SALT_SIZE; + salt_body_fido = Some(&enc_header[s..e]); + + let credential_id_fido_len: usize = enc_header[e].into(); + s = e + 1; + e = s + credential_id_fido_len; + credential_id_fido = Some(enc_header[s..e].to_vec()); + } - Ok((file_format_version, compress, archive)) + let enc_header_params = EncHeaderParams { + salt_body_pw, + salt_body_enc, + file_id, + salt_body_fido, + credential_id_fido, + compress, + archive, + keyfile, + usb_key + }; + Ok((file_format_version, enc_header_params)) } /// Decrypts a file encrypted with dual-layer encryption (AES-256-GCM-SIV + ChaCha20). /// - /// Reads and evaluates header from file, prompts user for password, derives master key using Argon2, - /// derives keys for ChaCha20 and AES-256-GCM-SIV, decrypts the file in chunks across multiple threads. - /// Output file has `.cce` suffix removed. + /// The input must end in `.cce` or `.c00` and contains: + /// - a short unencrypted header with salts for the password and header keys, + /// - an encrypted header that declares the body keys and file flags, + /// - the encrypted body stream, which may be compressed and/or archived. /// + /// The function validates the format version, derives the required keys from the + /// password and optional key material, decrypts the body in chunks, and writes the + /// result to the output directory or to a reconstructed file path. When compression + /// is enabled, it reassembles the compressed fragments and decompresses them before + /// writing. + /// /// # Arguments - /// - `filepath_in`: Path to encrypted input file (must end with `.cce`) - /// - `dirpath_out`: Option path to an output directory + /// - `filepath_in`: Path to encrypted input file (must end with `.cce` or `.c00`) + /// - `dirpath_out`: Option path to an output directory. If `None`, the current working directory is used. /// - `keyfilepath`: Optional path to an additional key file + /// - `verbose`: Verbose output + /// - `list_archive`: List contents of archive /// /// # Returns /// - `Ok(())` on successful decryption - /// - `Err` if file operations, password handling, or decryption fails + /// - `Err` for invalid extensions, missing required key material, unsupported format versions, + /// corrupted headers or payloads, and any I/O or write failure. pub fn decrypt(filepath_in: &PathBuf, dirpath_out: Option<&PathBuf>, keyfilepath: Option<&PathBuf>, verbose: bool, list_archive: bool) -> Result<()> { if filepath_in.is_dir() { return Err("Cannot decrypt a directory".into()); } + let mut filepath_out = PathBuf::from(filepath_in.file_name().unwrap()); + if filepath_in.extension() == Some(std::ffi::OsStr::new(ENCRYPTED_FILE_EXT)) || + filepath_in.extension() == Some(std::ffi::OsStr::new(SPLIT_ENC_FILE_EXT)) { + // remove encrypted-file-extension + filepath_out.set_extension(""); + } else { + return Err(format!("Invalid filename, it does not end with .{ENCRYPTED_FILE_EXT} or .{SPLIT_ENC_FILE_EXT}").into()) + } + // set read parameters - let mut read_input = Box:: new( ReadInput::new( - filepath_in, - CHUNK_SIZE + CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE, - HEADER_SIZE as u64 - )? ); - - // Read file header - let mut header = [0u8; HEADER_SIZE]; + let mut read_input = Box::new( + ReadInput::new( + filepath_in, + CHUNK_SIZE + CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE, + HEADER_SIZE as u64 + )? + ); + + // read file header + let mut header = vec![0u8; HEADER_SIZE]; read_input.read_files(&mut header)?; - // get salts from header - let (salt_pw, salt_cha, salt_aes) = Self::get_unencrypted_header_items(&header); + // get salts of first, unencrypted part of header + let (salt_header_pw, salt_header_enc) = Self::get_unencrypted_header_items(&mut header); - // get password and keys - let key = Self::hash_password(salt_pw, keyfilepath)?; - let (key_cha, key_aes) = Self::derive_keys(salt_cha, salt_aes, &key)?; - let cipher_cha = XChaCha20Poly1305::new_from_slice(key_cha.expose_secret())?; - let cipher_aes = Aes256GcmSiv::new_from_slice(key_aes.expose_secret())?; - - // get rest of header - let (file_format_version, compress, archive) = Self::get_encrypted_header_items(&header, &cipher_cha, &cipher_aes)?; + // get password and cipher structs for encrypted part of header + let mut key_builder = KeyBuilder::new(false); + let (cipher_header_cha, cipher_header_aes) = key_builder.get_password_and_create_header_ciphers(&salt_header_pw, &salt_header_enc)?; + + // decrypt second part of header + let (file_format_version, enc_header_params) = Self::get_encrypted_header_items(&mut header, &cipher_header_cha, &cipher_header_aes)?; // check format version if file_format_version != FILE_FORMAT_VERSION { @@ -390,20 +427,25 @@ impl Decryption { ).into()); } - let mut filepath_out = PathBuf::from(filepath_in.file_name().unwrap()); - if filepath_in.extension() == Some(std::ffi::OsStr::new(ENCRYPTED_FILE_EXT)) || - filepath_in.extension() == Some(std::ffi::OsStr::new(SPLIT_ENC_FILE_EXT)) { - // remove encrypted-file-extension - filepath_out.set_extension(""); - } else { - return Err(format!("Invalid filename, it does not end with .{ENCRYPTED_FILE_EXT} or .{SPLIT_ENC_FILE_EXT}").into()) + // check if a keyfile is required and provided by user + if enc_header_params.keyfile && keyfilepath.is_none() { + return Err("Keyfile missing".into()); + } + + // check for usb key/FIFO authenticator + if let Some(cred_id_fifo) = enc_header_params.credential_id_fido { + // usb key was used + assert!(enc_header_params.usb_key && enc_header_params.salt_body_fido.is_some() && key_builder.credential_id_fido.is_none()); + key_builder.credential_id_fido = Some(cred_id_fifo.to_vec()); } + let (cipher_body_cha, cipher_body_aes) = key_builder.create_body_ciphers(enc_header_params.salt_body_pw, enc_header_params.salt_body_enc, keyfilepath, enc_header_params.salt_body_fido)?; + // use output directory path ot current working directory let output_dir = if let Some(dir_out) = dirpath_out { // create user-specified output directory, if it is not an archive or archive is not just listed (and directory is missing) - if ((archive && !list_archive) || !archive) && !dir_out.exists() { + if ((enc_header_params.archive && !list_archive) || !enc_header_params.archive) && !dir_out.exists() { fs::create_dir_all(dir_out)?; } // canonicalize directory, as relative output path elements like ".." might not be resolved @@ -422,10 +464,12 @@ impl Decryption { if verbose { println!("--------------------------"); println!("File format version: {}", file_format_version); - println!("Compression: {}", compress); - println!("Archive: {}", archive); + println!("Keyfile: {}", enc_header_params.keyfile); + println!("USB security key: {}", enc_header_params.usb_key); + println!("Compression: {}", enc_header_params.compress); + println!("Archive: {}", enc_header_params.archive); println!("--------------------------"); - if archive { + if enc_header_params.archive { println!("Archive file is extracted to directory {}", output_dir.display()); } else { println!("Output is written to file {}", filepath_out.display()); @@ -433,7 +477,7 @@ impl Decryption { } // set write parameters and create output file - let write_output: Box = if archive { + let write_output: Box = if enc_header_params.archive { // convert relative path elements to absolute ones (looks better for verbose print) // in list mode, directory might not exist let dirpath_out_absolute = dirpath_out.map(|p| @@ -444,7 +488,13 @@ impl Decryption { Box::new( WriteOutput::new(filepath_out, vec![])? ) }; - CryptIo::io_chunks(&cipher_cha, &cipher_aes, compress, Self::decrypt_pipe, read_input, write_output)?; + let crypt_cfg = CryptConfig { + cipher_cha: cipher_body_cha, + cipher_aes: cipher_body_aes, + compress: enc_header_params.compress, + more_associated_data: enc_header_params.file_id.to_vec() + }; + CryptIo::io_chunks(&crypt_cfg, Self::decrypt_pipe, read_input, write_output)?; Ok(()) } @@ -459,82 +509,34 @@ impl Decryption { #[cfg(test)] mod tests { use super::*; - use std::fs; - use rand::Rng; - use crate::SALT_SIZE; + use std::path; #[test] - fn test_hash_password_decrypt() { - let salt = [1u8; SALT_SIZE]; - // key should be the same for each call - let key1 = Decryption::hash_password(&salt ,None).unwrap(); - let key2 = Decryption::hash_password(&salt, None).unwrap(); - assert_eq!(key1.expose_secret(), key2.expose_secret()); - // key should not be all zeros - assert_ne!(key1.expose_secret(), [0u8; KEY_SIZE]); - - // key should change for the same salt, if a keyfile is added - let filepath_kf = PathBuf::from("test_pw_key.bin"); - let mut data_kf = vec![0; 100]; - rand::rng().fill_bytes(&mut data_kf); - fs::write(&filepath_kf, &data_kf).unwrap(); - - let key3 = Decryption::hash_password(&salt, Some(&filepath_kf)).unwrap(); - assert_ne!(key1.expose_secret(), key3.expose_secret()); - - // key file does not exist - assert!(Decryption::hash_password(&salt, Some(&PathBuf::from("test_miss"))).is_err()); - - // cleanup - let _ = fs::remove_file(&filepath_kf); + fn test_get_unencrypted_header_items() { + let hdr_org = [[1u8; SALT_SIZE], [2u8; SALT_SIZE], [3u8; SALT_SIZE]].concat(); + let mut hdr = hdr_org.clone(); + let (salt1, salt2) = Decryption::get_unencrypted_header_items(&mut hdr); + assert_eq!(hdr.len(), SALT_SIZE); + assert_eq!(salt1, hdr_org[..SALT_SIZE]); + assert_eq!(salt2, hdr_org[SALT_SIZE..2 * SALT_SIZE]); + assert_eq!(hdr, hdr_org[2 * SALT_SIZE..]) } #[test] - fn test_derive_keys_decrypt() { - let key: [u8; 32] = rand::random(); - let key_sec = SecretSlice::from(key.to_vec()); - let salt_cha: [u8; SALT_SIZE] = rand::random(); - let salt_aes: [u8; SALT_SIZE] = rand::random(); + fn test_decrypt() { + // most tests are done together with encryption test - let (key_cha, key_aes) = Decryption::derive_keys(&salt_cha, &salt_aes, &key_sec).unwrap(); + // file for decryption is too small + let filepath_in = path::absolute(PathBuf::from("test_dec.cce")).unwrap(); + let data = vec![0xa5; HEADER_SIZE + CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE - 1]; + fs::write(&filepath_in, &data).unwrap(); + assert!(Decryption::decrypt(&filepath_in, None, None, false, false).is_err()); - // Output should have correct sizes - assert_eq!(key_cha.expose_secret().len(), KEY_SIZE); - assert_eq!(key_aes.expose_secret().len(), KEY_SIZE); - - // Keys should not be all zeros - assert_ne!(key_cha.expose_secret(), [0u8; KEY_SIZE]); - assert_ne!(key_aes.expose_secret(), [0u8; KEY_SIZE]); - - // ChaCha and AES keys should be different from each other - assert_ne!(key_cha.expose_secret(), key_aes.expose_secret()); - - // Same inputs should produce identical outputs - let (key_cha2, key_aes2) = Decryption::derive_keys(&salt_cha, &salt_aes, &key_sec).unwrap(); - assert_eq!(key_cha.expose_secret(), key_cha2.expose_secret()); - assert_eq!(key_aes.expose_secret(), key_aes2.expose_secret()); - - // Different ChaCha salt - let mut salt_cha2 = salt_cha; - salt_cha2[0] ^= 0xFF; - let (key_cha3, key_aes3) = Decryption::derive_keys(&salt_cha2, &salt_aes, &key_sec).unwrap(); - assert_ne!(key_cha.expose_secret(), key_cha3.expose_secret()); - assert_eq!(key_aes.expose_secret(), key_aes3.expose_secret()); - - // Different AES salt - let mut salt_aes4 = salt_aes; - salt_aes4[0] ^= 0xFF; - let (key_cha4, key_aes4) = Decryption::derive_keys(&salt_cha, &salt_aes4, &key_sec).unwrap(); - assert_eq!(key_cha.expose_secret(), key_cha4.expose_secret()); - assert_ne!(key_aes.expose_secret(), key_aes4.expose_secret()); - - // Different input key - let mut key2 = key; - key2[0] ^= 0xFF; - let key_sec2 = SecretSlice::from(key2.to_vec()); - let (key_cha5, key_aes5) = Decryption::derive_keys(&salt_cha, &salt_aes, &key_sec2).unwrap(); - assert_ne!(key_cha.expose_secret(), key_cha5.expose_secret()); - assert_ne!(key_aes.expose_secret(), key_aes5.expose_secret()); + // empty .cce-file + fs::write(&filepath_in, []).unwrap(); + assert!(Decryption::decrypt(&filepath_in, None, None, false, false).is_err()); + // cleanup + let _ = fs::remove_file(&filepath_in); } } \ No newline at end of file diff --git a/src/encryption.rs b/src/encryption.rs index 2b18dfd..43bb7db 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -2,24 +2,50 @@ use std::io::Read; use std::path::{Path, PathBuf}; use std::{fs, thread}; use std::env; -use argon2::Argon2; +use std::time::SystemTime; use chacha20poly1305::{XChaCha20Poly1305, XNonce}; +use aes_gcm_siv::{aead::Generate, Aes256GcmSiv, AeadInOut, Nonce}; +use sha2::{Sha256, Digest}; use rand::{Rng, SeedableRng}; use rand::rngs::SysRng; use rand_chacha::ChaCha20Rng; -use aes_gcm_siv::{aead::{KeyInit, Generate}, Aes256GcmSiv, AeadInOut, Nonce}; -use secrecy::{ExposeSecret, ExposeSecretMut, SecretSlice}; use bzip2::Compression; use bzip2::read::BzEncoder; use crossbeam_channel::{bounded, Sender, Receiver}; use std::collections::HashMap; -use crate::{Result, SALT_SIZE, KEY_SIZE, CHA_NONCE_SIZE, CHA_TAG_SIZE, AES_NONCE_SIZE, AES_TAG_SIZE, CHUNK_SIZE, - COMPRESS_LENGTH_SIZE, ENCRYPTED_FILE_EXT, SPLIT_ENC_FILE_EXT, HEADER_SIZE, FILE_FORMAT_VERSION}; -use crate::common::{get_pass_bytes, key_derivation}; -use crate::common_io::{CryptIo, ReadInput, WriteOutput, ReadChunk, WriteFiles}; +use crate::{AES_NONCE_SIZE, AES_TAG_SIZE, CHA_NONCE_SIZE, CHA_TAG_SIZE, CHUNK_SIZE, FILE_ID_SIZE, COMPRESS_LENGTH_SIZE, + ENCRYPTED_FILE_EXT, FILE_FORMAT_VERSION, HEADER_SIZE, MAX_CREDENTIAL_ID_FIDO_SIZE, Result, SALT_SIZE, SPLIT_ENC_FILE_EXT}; +use crate::common::KeyBuilder; +use crate::common_io::{CryptIo, CryptConfig, ReadInput, WriteOutput, ReadChunk, WriteFiles}; use crate::archive::ArchiveRead; + +/// Parameters embedded in the encrypted header payload for the body encryption. +/// +/// These values describe the per-file configuration and the salts used to derive +/// the body keys. +pub struct EncHeaderParams<'a> { + /// Salt used to derive the password-based body key. + pub salt_body_pw: &'a [u8], + /// Salt used to derive the body encryption key. + pub salt_body_enc: &'a [u8], + /// File ID for authentication of encrypted data + pub file_id: &'a [u8], + /// Optional salt for a USB security key/FIFO authenticator. + pub salt_body_fido: Option<&'a [u8]>, + /// Optional FIDO credential ID stored in the encrypted header. + pub credential_id_fido: Option>, + /// Whether the body data should be compressed before encryption. + pub compress: bool, + /// Whether the input was an archive. + pub archive: bool, + /// Whether an external keyfile was used. + pub keyfile: bool, + /// Whether a USB security key was required. + pub usb_key: bool, +} + /// Handles file encryption operations using dual-layer encryption and compression. /// /// Combines ChaCha20-Poly1305 and AES-256-GCM-SIV for encryption. @@ -33,7 +59,6 @@ impl Encryption { /// derivation routines. /// /// # Returns - /// /// - `Ok([u8; SALT_SIZE])` newly generated salt on success. /// - `Err` if RNG initialization fails. fn create_salt() -> Result<[u8; SALT_SIZE]> { @@ -43,51 +68,29 @@ impl Encryption { Ok(salt) } - - /// Hashes a user-provided password and an optional user-provided key file using Argon2. + + /// Generate a unique file identifier for encrypted payloads. /// - /// Generates a random salt and derives a cryptographic key from the password and, if available, - /// the key file using the Argon2id password hashing algorithm. + /// The identifier is derived from a fresh random salt and the current UNIX + /// timestamp in nanoseconds, both hashed via SHA-256. This yields a stable + /// `FILE_ID_SIZE` value that is unique enough to distinguish different + /// encrypted files while still being compact for header metadata. /// - /// # Arguments - /// - `keyfilepath`: Optional path to an additional key file - /// /// # Returns - /// - `Ok((salt_pw, key))` containing the salt and derived key - /// - `Err` if password hashing or getting password/key file fails - fn hash_password(keyfilepath: Option<&PathBuf>) -> Result<([u8; SALT_SIZE], SecretSlice)> { - let pass = get_pass_bytes(keyfilepath, true)?; + /// - `Ok([u8; FILE_ID_SIZE])` containing the generated file ID. + /// - `Err` if obtaining the current time or generating the salt fails. + fn create_file_id() -> Result<[u8; FILE_ID_SIZE]> { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_nanos(); - let salt_pw = Self::create_salt()?; - let mut key = SecretSlice::from(vec![0u8; KEY_SIZE]); - Argon2::default().hash_password_into(pass.expose_secret(), &salt_pw, key.expose_secret_mut()) - .map_err(|e| format!("Failed to hash password: {:?}", e))?; + let mut hasher = Sha256::new(); + hasher.update(Self::create_salt()?); + hasher.update(now.to_le_bytes()); + let file_id = hasher.finalize(); - Ok((salt_pw, key)) + Ok(file_id.into()) } - /// Derive independent ChaCha20 and AES keys and their salts from a master key. - /// - /// Generates two independent random salts and expands the provided `key` with - /// HKDF-SHA256 to produce two 32-byte keys. - /// - /// # Arguments - /// - `key`: master secret material to expand (kept in `SecretSlice`). - /// - /// # Returns - /// - `Ok(( [u8; SALT_SIZE], SecretSlice, [u8; SALT_SIZE], SecretSlice ))` on success. - /// - `Err` if HKDF expansion or random salt generation fails. - #[allow(clippy::type_complexity)] - fn derive_keys(key: &SecretSlice) -> Result<([u8; SALT_SIZE], SecretSlice, [u8; SALT_SIZE], SecretSlice)> { - let salt_cha = Self::create_salt()?; - let salt_aes = Self::create_salt()?; - let key_cha = key_derivation(key, &salt_cha, "xchacha20poly1305".as_bytes())?; - let key_aes = key_derivation(key, &salt_aes, "-aes-256-gcm-siv-".as_bytes())?; - - Ok((salt_cha, key_cha, salt_aes, key_aes)) - } - /// Encrypt a single chunk with XChaCha20-Poly1305 and per-chunk sequencing info. /// /// This generates a fresh random nonce and then computes a modified nonce used @@ -107,19 +110,27 @@ impl Encryption { /// This construction aims to provide sequence/truncation /// protection by deriving a per-chunk nonce from the random base nonce plus /// explicit chunk metadata. + /// + /// In addition to the nonce, the function authenticates an `associated_data` + /// vector composed of the file format version, a characterizing string, and a + /// caller-provided array. This binds the encrypted chunk to + /// the file format and any additional per-file metadata used by the caller. /// /// # Arguments /// - `cipher`: XChaCha20-Poly1305 cipher struct initialized with key. - /// - `buf_inout`: data-in: plaintext bytes to encrypt (one chunk); data-out: containing ciphertext (+ authentication tag) + nonce + /// - `buf_inout`: data-in: plaintext bytes to encrypt (one chunk); + /// data-out: containing ciphertext (+ authentication tag) + nonce /// - `chunk_count`: zero-based chunk index (incremented per chunk). Must be /// the same value used when decrypting this chunk. /// - `final_chunk`: `true` if this is the last chunk of the file, /// `false` otherwise. Also must match the value used at decryption. + /// - `more_associated_data`: Additional per-file metadata folded into the + /// authenticated `associated_data` for this chunk. /// /// # Returns /// - `Ok()` on successful encryption /// - `Err(...)` if encryption fails - pub fn cha_encrypt_buffer(cipher: &XChaCha20Poly1305, buf_inout: &mut Vec, chunk_count: u32, final_chunk: bool) -> Result<()> { + pub fn cha_encrypt_buffer(cipher: &XChaCha20Poly1305, buf_inout: &mut Vec, chunk_count: u32, final_chunk: bool, more_associated_data: &[u8]) -> Result<()> { let mut nonce = XNonce::try_generate()?; let nonce_org = nonce; @@ -129,7 +140,13 @@ impl Encryption { for (i, ccb) in chunk_count.to_le_bytes().iter().enumerate() { nonce[i+1] ^= ccb; } - cipher.encrypt_in_place(&nonce, &[], buf_inout)?; + + let mut associated_data = vec![FILE_FORMAT_VERSION]; + associated_data.extend("Cha".as_bytes()); + associated_data.extend(more_associated_data); + + cipher.encrypt_in_place(&nonce, &associated_data, buf_inout)?; + buf_inout.extend(nonce_org); Ok(()) } @@ -139,17 +156,38 @@ impl Encryption { /// Generates a random nonce and encrypts the buffer. The output includes /// the ciphertext and the nonce for transmission. /// The nonce is appended (and not prepended) for faster execution. + /// + /// The authenticated associated data is built from the file format version, a + /// characterizing string, any caller-supplied array, the + /// little-endian bytes of `chunk_count`, and the `final_chunk` flag. This + /// binds the encrypted chunk to the file format, per-file metadata, and the + /// chunk sequence information. /// /// # Arguments /// - `cipher`: AES-256-GCM-SIV cipher struct initialized with key. - /// - `buf_inout`: data-in: plaintext bytes to encrypt (one chunk); data-out: containing ciphertext (+ authentication tag) + nonce + /// - `buf_inout`: data-in: plaintext bytes to encrypt (one chunk); + /// data-out: containing ciphertext (+ authentication tag) + nonce + /// - `chunk_count`: zero-based chunk index (incremented per chunk). Must be + /// the same value used when decrypting this chunk. + /// - `final_chunk`: `true` if this is the last chunk of the file, + /// `false` otherwise. Also must match the value used at decryption. + /// - `more_associated_data`: Additional per-file metadata folded into the + /// authenticated `associated_data` for this chunk. /// /// # Returns /// - `Ok()` on successful encryption /// - `Err(...)` if encryption fails - pub fn aes_encrypt_buffer(cipher: &Aes256GcmSiv, buf_inout: &mut Vec) -> Result<()> { + pub fn aes_encrypt_buffer(cipher: &Aes256GcmSiv, buf_inout: &mut Vec, chunk_count: u32, final_chunk: bool, more_associated_data: &[u8]) -> Result<()> { let nonce = Nonce::try_generate()?; - cipher.encrypt_in_place(&nonce, &[], buf_inout)?; + + let mut associated_data = vec![FILE_FORMAT_VERSION]; + associated_data.extend("Aes".as_bytes()); + associated_data.extend(more_associated_data); + associated_data.extend(chunk_count.to_le_bytes()); + associated_data.push(u8::from(final_chunk)); + + cipher.encrypt_in_place(&nonce, &associated_data, buf_inout)?; + buf_inout.extend(&nonce); Ok(()) } @@ -227,38 +265,41 @@ impl Encryption { Ok(()) } - /// Creates the multithreaded encryption pipeline and returns thread handles. + /// Creates the multithreaded encryption pipeline and returns all worker handles. + /// + /// Incoming plaintext chunks are read from `rx_in` and forwarded to either a + /// compression/reserialization pipeline or directly to the encryption workers. + /// The implementation allocates a dedicated resegmentation stage when compression + /// is enabled, and always keeps the final output order stable by sending + /// `(Vec, u32)` records to `tx_out` in chunk index order. + /// + /// When compression is enabled, the function behaves as follows: + /// - allocates more compression threads than encryption threads + /// - each compression worker reads `(Vec, u32, bool)` from `rx_in`, calls + /// `compress_buffer`, and forwards the compressed bytes as `(Vec, u32, bool)` + /// to a bounded internal channel + /// - a single resegmentation thread consumes this channel, concatenates and + /// packetizes compressed fragments using the length-prefix scheme, and emits + /// the resulting `(Vec, u32, bool)` stream into the encryption channel + /// - every encryption worker reads from that channel, encrypts the data with + /// XChaCha20-Poly1305 encryption, then runs AES-256-GCM-SIV on the result before + /// sending the final payload to `tx_out` /// - /// Spawns worker threads to process incoming plaintext chunks from `rx_in` and - /// send encrypted chunks to `tx_out`. Behavior depends on `compress`: - /// - If `compress == true`: - /// - Spawns `cpu_count` compression threads. Each compression thread reads - /// `(Vec, u32, bool)` from `rx_in`, compresses the chunk and sends - /// `(Vec, u32, bool)` into an internal compression channel. - /// - Spawns one resegmentation thread that reads ordered compressed pieces - /// from the compression channel, packs length-prefixed compressed fragments - /// into CHUNK_SIZE-sized output buffers (preserving sequence), and forwards - /// `(Vec, u32, bool)` into the encryption channel. - /// - Regardless of `compress`, spawns `cpu_count` encryption threads. Each - /// encryption thread reads `(Vec, u32, bool)` from the encryption channel - /// (or directly from `rx_in` when `compress == false`), applies the per-chunk - /// XChaCha20-Poly1305 encryption (including chunk_count/final_chunk sequencing), - /// then AES-256-GCM-SIV on the result, and sends `(Vec, u32)` to `tx_out`. + /// When compression is disabled, all worker slots are used for encryption and + /// each thread reads directly from `rx_in`. /// /// # Arguments - /// - `cipher_cha`: cipher struct for XChaCha20-Poly1305 encryption - /// - `cipher_aes`: cipher struct for AES-256-GCM-SIV encryption - /// - `compress`: enable compression + resegment stage when true. - /// - `rx_in`: receiver for input plaintext chunks `(data, chunk_count, final_flag)`. - /// - `tx_out`: sender for encrypted output `(encrypted_chunk, chunk_count)`. - /// - `cpu_count`: number of worker threads to spawn. + /// - `crypt_cfg`: Shared configuration holding the active ciphers, compression flag, + /// and associated data for the encrypted chunks. + /// - `rx_in`: Receiver of plaintext chunks as `(Vec, chunk_count, final_chunk)`. + /// - `tx_out`: Sender for encrypted output chunks as `(Vec, chunk_count)`. + /// - `cpu_count`: Number of logical CPU cores available to size the worker pool. /// /// # Returns - /// - `Vec>>` handles for all spawned threads. + /// - `Vec>>` containing all spawned worker + /// threads for compression, resegmentation, and encryption. pub fn encrypt_pipe( - cipher_cha: &XChaCha20Poly1305, - cipher_aes: &Aes256GcmSiv, - compress: bool, + crypt_cfg: &CryptConfig, rx_in: Receiver<(Vec, u32, bool)>, tx_out: Sender<(Vec, u32)>, cpu_count: usize @@ -267,7 +308,7 @@ impl Encryption { // number of threads used for encryption and compression let c_thread_cnt; let e_thread_cnt; - if compress { + if crypt_cfg.compress { c_thread_cnt = 1.max(cpu_count * 9 / 10); e_thread_cnt = 1.max(cpu_count - c_thread_cnt); } else { @@ -279,7 +320,7 @@ impl Encryption { let mut thread_handles = Vec::with_capacity(cpu_count * 2 + 1); - if compress { + if crypt_cfg.compress { let (tx_c, rx_c) = bounded(cpu_count); // compression threads @@ -309,16 +350,19 @@ impl Encryption { // encryption threads for _ in 0..e_thread_cnt { - let cipher_cha= cipher_cha.clone(); - let cipher_aes = cipher_aes.clone(); - let rx_in = if compress { rx_e.clone() } else { rx_in.clone() }; + let cipher_cha= crypt_cfg.cipher_cha.clone(); + let cipher_aes = crypt_cfg.cipher_aes.clone(); + let more_associated_data = crypt_cfg.more_associated_data.clone(); + let rx_in = if crypt_cfg.compress { rx_e.clone() } else { rx_in.clone() }; let tx_out = tx_out.clone(); thread_handles.push(thread::spawn( move || -> std::result::Result<(), String> { for (mut buf_inout, chunk_count, final_chunk) in rx_in { buf_inout.reserve(CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE); - Self::cha_encrypt_buffer(&cipher_cha, &mut buf_inout, chunk_count, final_chunk).map_err(|e| e.to_string())?; - Self::aes_encrypt_buffer(&cipher_aes, &mut buf_inout).map_err(|e| e.to_string())?; + Self::cha_encrypt_buffer(&cipher_cha, &mut buf_inout, chunk_count, final_chunk, &more_associated_data) + .map_err(|e| e.to_string())?; + Self::aes_encrypt_buffer(&cipher_aes, &mut buf_inout, chunk_count, final_chunk, &more_associated_data) + .map_err(|e| e.to_string())?; if tx_out.send((buf_inout, chunk_count)).is_err() { break } } Ok(()) @@ -388,86 +432,117 @@ impl Encryption { Ok((filepath_out, build_archive, strip_dir_in)) } - /// Creates header of output file. - /// - /// Salts are required unencrypted, the other data is encrypted. - /// + /// Builds the encrypted file header for a new output file or archive. + /// + /// The header consists of: + /// - a cleartext prefix containing the password salt and the encryption salt + /// - an encrypted metadata block containing: + /// - the file format version + /// - feature flags such as compression, archive mode, keyfile usage, and USB-key usage + /// - the body salts used for the main encryption key derivation + /// - the optional FIDO salt + /// - the optional FIDO credential ID, padded with random bytes to a fixed length + /// + /// The metadata block is first encrypted with the header-specific + /// XChaCha20-Poly1305 and AES-256-GCM-SIV ciphers, and the resulting bytes are + /// appended to the cleartext salts. This preserves the salts needed for early + /// key derivation while keeping the rest of the header hidden behind the + /// two-layer AEAD encryption. + /// /// # Arguments - /// - `salt_pw`: salt of password hash - /// - `salt_cha`: salt of the ChaCha key derivation - /// - `salt_aes`: salt of the AES key derivation - /// - `compress`: compression enabled - /// - `archive`: archiving enabled - /// - `cipher_cha`: cipher struct for ChaCha encryption - /// - `cipher_aes`: cipher struct for AES encryption - /// + /// - `salt_header_pw`: salt used when deriving the password-based header key + /// - `salt_header_enc`: salt used when deriving the encryption-based header key + /// - `enc_params`: flags and salts embedded in the encrypted header metadata + /// - `cipher_cha`: ChaCha20-Poly1305 cipher used for the encrypted header block + /// - `cipher_aes`: AES-256-GCM-SIV cipher used for the encrypted header block + /// /// # Returns - /// - `Ok(header)` contains header on success - /// - `Err` if encryption or random data generation fails - fn create_header(salt_pw: &[u8], salt_cha: &[u8], salt_aes: &[u8], compress: bool, archive: bool, cipher_cha: &XChaCha20Poly1305, cipher_aes: &Aes256GcmSiv) -> Result> { + /// - `Ok(header)`: the final header bytes + /// - `Err`: if encryption or random data generation fails + fn create_header( + salt_header_pw: &[u8], + salt_header_enc: &[u8], + enc_params: EncHeaderParams, + cipher_cha: &XChaCha20Poly1305, + cipher_aes: &Aes256GcmSiv + ) -> Result> { let mut header = Vec::with_capacity(HEADER_SIZE); // // cleartext header part // - header.extend(salt_pw); - header.extend(salt_cha); - header.extend(salt_aes); + header.extend(salt_header_pw); + header.extend(salt_header_enc); // // encrypted header part // + let mut buf_inout = vec![ + FILE_FORMAT_VERSION, + u8::from(enc_params.compress) | (u8::from(enc_params.archive) << 1) | (u8::from(enc_params.keyfile) << 2) | (u8::from(enc_params.usb_key) << 3), + ]; + // these salts are in the encrypted part just because of the same context: encrypted body data + buf_inout.extend(enc_params.salt_body_pw); + buf_inout.extend(enc_params.salt_body_enc); + // file ID + buf_inout.extend(enc_params.file_id); + + if let Some(salt_fido) = enc_params.salt_body_fido { + buf_inout.extend(salt_fido); + } else { + // if usb_key/salt_body_fido is not used, create a salt for it as a filler + buf_inout.extend(Self::create_salt()?); + } - // random filler data - let mut random_data = [0u8; 2]; + let credential_id_fido = enc_params.credential_id_fido.unwrap_or_default(); + + // length of fido credential + buf_inout.push(u8::try_from(credential_id_fido.len())?); + // fido credential + buf_inout.extend(&credential_id_fido); + + // fill unused size of credential_id_fido with random data + let mut random_data = vec![0u8; MAX_CREDENTIAL_ID_FIDO_SIZE - credential_id_fido.len()]; let mut rng = ChaCha20Rng::try_from_rng(&mut SysRng)?; rng.fill_bytes(&mut random_data); + buf_inout.extend(random_data); - // add some randomness to the constant data - let mut buf_inout = vec![ - random_data[0], - FILE_FORMAT_VERSION, - random_data[1], - u8::from(compress) | (u8::from(archive) << 1), - ]; buf_inout.reserve(CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE); - Self::cha_encrypt_buffer(cipher_cha, &mut buf_inout, u32::MAX, false)?; - Self::aes_encrypt_buffer(cipher_aes, &mut buf_inout)?; + Self::cha_encrypt_buffer(cipher_cha, &mut buf_inout, 0, false, "header_cha".as_bytes())?; + Self::aes_encrypt_buffer(cipher_aes, &mut buf_inout, u32::MAX, true, "header_aes".as_bytes())?; header.extend(buf_inout); + + assert_eq!(header.len(), HEADER_SIZE); Ok(header) } - /// Encrypts a file or a directory using dual-layer encryption (ChaCha20 + AES-256-GCM-SIV) - /// with optional compression. + /// Encrypts a file or a directory using dual-layer encryption (ChaCha20 + AES-256-GCM-SIV). /// - /// Prompts user for password, derives master key using Argon2, derives keys for - /// ChaCha20 and AES-256-GCM-SIV, compresses (on demand) and encrypts the file/directory in - /// chunks across multiple threads. Output file gets `.cce` extension. Or output can - /// be split into several files, which get extensions `.c00`, `.c01`, `.c02`, ... + /// The method generates fresh salts and keys, writes a file header containing the + /// format metadata, and then encrypts the input stream in chunks. If compression is + /// enabled, each plaintext chunk is bzip2-compressed, re-segmented to fixed-size blocks, + /// and then encrypted. Output can be written as a single `.cce` file or as split files + /// named with `.c00`, `.c01`, `.c02`, and so on. /// /// # Arguments - /// - `filepath_in`: Path to input file or directory to encrypt - /// - `dirpath_out`: Optional path to an output directory - /// - `keyfilepath`: Optional path to an additional key file + /// - `filepath_in`: Path to input file or directory to encrypt. + /// - `dirpath_out`: Optional path to an output directory. If `None`, the current working directory is used. + /// - `keyfilepath`: Optional path to an additional key file. + /// - `usb_key`: Use additional USB security key /// - `compress`: Compress input file before encryption - /// - `split`: List of output split sizes; if empty, no split is done. + /// - `split`: List of output split sizes; if empty, output is not split. + /// - `verbose`: Verbose output /// /// # Returns /// - `Ok(())` on successful encryption /// - `Err` if file operations, password handling, or encryption fails - pub fn encrypt(filepath_in: &PathBuf, dirpath_out: Option<&PathBuf>, keyfilepath: Option<&PathBuf>, compress: bool, split: Vec, verbose: bool) -> Result<()> { + pub fn encrypt(filepath_in: &PathBuf, dirpath_out: Option<&PathBuf>, keyfilepath: Option<&PathBuf>, usb_key: bool, compress: bool, split: Vec, verbose: bool) -> Result<()> { let (mut filepath_out, build_archive, strip_dir) = Self::set_paths(filepath_in, &env::current_dir()?, dirpath_out, split.is_empty())?; - // ask for password, before there can be error messages of archive - let (salt_pw, key) = Self::hash_password(keyfilepath)?; - let (salt_cha, key_cha, salt_aes, key_aes) = Self::derive_keys(&key)?; - let cipher_cha = XChaCha20Poly1305::new_from_slice(key_cha.expose_secret())?; - let cipher_aes = Aes256GcmSiv::new_from_slice(key_aes.expose_secret())?; - // output directory path is used // create a new directory after password entry: // if password entry failed or user breaks execution at password entry, filesystem stays unchanged @@ -478,12 +553,37 @@ impl Encryption { // canonicalize directory part, as relative output path elements like ".." might not be resolved filepath_out = filepath_out.parent().unwrap().canonicalize()?.join(filepath_out.file_name().unwrap()); + // ask for password, before archive threads are started and hence error messages of archive could occur + let mut key_builder = KeyBuilder::new(true); + let salt_header_pw = Self::create_salt()?; + let salt_header_enc = Self::create_salt()?; + let (cipher_header_cha, cipher_header_aes) = key_builder.get_password_and_create_header_ciphers(&salt_header_pw, &salt_header_enc)?; + + let salt_body_pw = Self::create_salt()?; + let salt_body_enc = Self::create_salt()?; + let mut salt_body_fido: Option<&[u8]> = None; + let sbf; + if usb_key { + sbf = Self::create_salt()?; + salt_body_fido = Some(&sbf); + } + assert!(key_builder.credential_id_fido.is_none()); + let (cipher_body_cha, cipher_body_aes) = key_builder.create_body_ciphers(&salt_body_pw, &salt_body_enc, keyfilepath, salt_body_fido)?; + if usb_key { + assert!(key_builder.credential_id_fido.is_some() && salt_body_fido.is_some()); + } else { + assert!(key_builder.credential_id_fido.is_none() && salt_body_fido.is_none()); + } + + // show verbose prints after password entry, like in decryption if verbose { - println!("------------------------"); + println!("--------------------------"); println!("File format version: {}", FILE_FORMAT_VERSION); - println!("Compression: {}", if compress {"on"} else {"off"} ); - println!("Archive: {}", if build_archive {"on"} else {"off"} ); - println!("------------------------"); + println!("Keyfile: {}", keyfilepath.is_some()); + println!("USB security key: {}", usb_key); + println!("Compression: {}", compress); + println!("Archive: {}", build_archive); + println!("--------------------------"); println!("Output is written to file {}", filepath_out.display()); } @@ -492,15 +592,34 @@ impl Encryption { } else { Box::new( ReadInput::new(filepath_in, CHUNK_SIZE, 0)? ) }; - + // set write parameters and create output file let mut write_output = Box::new( WriteOutput::new(filepath_out, split)? ); - + + let file_id = Self::create_file_id()?; + // create and write header - let header = Self::create_header(&salt_pw, &salt_cha, &salt_aes, compress, build_archive, &cipher_cha, &cipher_aes)?; + let enc_header_params = EncHeaderParams { + salt_body_pw: &salt_body_pw, + salt_body_enc: &salt_body_enc, + file_id: &file_id, + salt_body_fido, + credential_id_fido: key_builder.credential_id_fido, + compress, + archive: build_archive, + keyfile: keyfilepath.is_some(), + usb_key + }; + let header = Self::create_header(&salt_header_pw, &salt_header_enc, enc_header_params, &cipher_header_cha, &cipher_header_aes)?; write_output.write_files(&header)?; - CryptIo::io_chunks(&cipher_cha, &cipher_aes, compress, Self::encrypt_pipe, read_input, write_output)?; + let crypt_cfg = CryptConfig { + cipher_cha: cipher_body_cha, + cipher_aes: cipher_body_aes, + compress, + more_associated_data: file_id.to_vec() + }; + CryptIo::io_chunks(&crypt_cfg, Self::encrypt_pipe, read_input, write_output)?; Ok(()) } @@ -516,55 +635,33 @@ mod tests { use super::*; use std::fs; use std::path; + use secrecy::{ExposeSecret, ExposeSecretMut, SecretSlice}; + use aes_gcm_siv::KeyInit; use crate::decryption::Decryption; #[test] - fn test_hash_password_encrypt() { - // salt and key should differ for each call - let (salt1, key1) = Encryption::hash_password(None).unwrap(); - let (salt2, key2) = Encryption::hash_password(None).unwrap(); - assert_ne!(salt1, salt2); - assert_ne!(key1.expose_secret(), key2.expose_secret()); - // salt, key should not be all zeros - assert_ne!(salt1, [0u8; SALT_SIZE]); - assert_ne!(key1.expose_secret(), [0u8; KEY_SIZE]); - - // key file does not exist - assert!(Encryption::hash_password(Some(&PathBuf::from("test_miss"))).is_err()); + fn test_create_salt() { + let salt_1 = Encryption::create_salt().unwrap(); + let salt_2 = Encryption::create_salt().unwrap(); + + assert_ne!(salt_1, salt_2); + assert_eq!(salt_1.len(), SALT_SIZE); + assert_eq!(salt_2.len(), SALT_SIZE); + assert_ne!(salt_1, [0u8; SALT_SIZE]); + assert_ne!(salt_2, [0u8; SALT_SIZE]); } #[test] - fn test_derive_keys_encrypt() { - let key: [u8; 32] = rand::random(); - let key_sec = SecretSlice::from(key.to_vec()); - - let (salt_cha, key_cha, salt_aes, key_aes) = Encryption::derive_keys(&key_sec).unwrap(); - // Check sizes - assert_eq!(salt_cha.len(), SALT_SIZE); - assert_eq!(salt_aes.len(), SALT_SIZE); - assert_eq!(key_cha.expose_secret().len(), KEY_SIZE); - assert_eq!(key_aes.expose_secret().len(), KEY_SIZE); - - // ChaCha and AES keys should be different from each other - assert_ne!(key_cha.expose_secret(), key_aes.expose_secret()); - - // Salts should not be all zeros - assert_ne!(salt_cha, [0u8; SALT_SIZE]); - assert_ne!(salt_aes, [0u8; SALT_SIZE]); - - // Keys should not be all zeros - assert_ne!(key_cha.expose_secret(), [0u8; KEY_SIZE]); - assert_ne!(key_aes.expose_secret(), [0u8; KEY_SIZE]); - - // The two salts should be different from each other - assert_ne!(salt_cha, salt_aes); - - // Salts and keys should be different on each call - let (salt_cha2, key_cha2, salt_aes2, key_aes2) = Encryption::derive_keys(&key_sec).unwrap(); - assert_ne!(salt_cha, salt_cha2); - assert_ne!(salt_aes, salt_aes2); - assert_ne!(key_cha.expose_secret(), key_cha2.expose_secret()); - assert_ne!(key_aes.expose_secret(), key_aes2.expose_secret()); + fn test_create_file_id() { + let file_id_1 = Encryption::create_file_id().unwrap(); + std::thread::sleep(std::time::Duration::from_millis(1)); + let file_id_2 = Encryption::create_file_id().unwrap(); + + assert_ne!(file_id_1, file_id_2); + assert_eq!(file_id_1.len(), FILE_ID_SIZE); + assert_eq!(file_id_2.len(), FILE_ID_SIZE); + assert_ne!(file_id_1, [0u8; FILE_ID_SIZE]); + assert_ne!(file_id_2, [0u8; FILE_ID_SIZE]); } #[test] @@ -575,16 +672,16 @@ mod tests { let data: [u8; 100] = rand::random(); let mut encrypt_data = data.to_vec(); - Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, false).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, false, &[]).unwrap(); assert_eq!(encrypt_data.len(), data.len() + CHA_NONCE_SIZE + CHA_TAG_SIZE); let mut decrypt_data = encrypt_data.clone(); - Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, false).unwrap(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, false, &[]).unwrap(); assert_eq!(data, decrypt_data[..]); // second encryption must produce different output of the same input let mut encrypt_data2 = data.to_vec(); - Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data2, 0, false).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data2, 0, false, &[]).unwrap(); // nonce part (at the end) must be different assert_ne!( encrypt_data[encrypt_data.len() - CHA_NONCE_SIZE..], @@ -600,67 +697,84 @@ mod tests { let mut bad_data = encrypt_data.clone(); let last_idx = bad_data.len() - 1; bad_data[last_idx] ^= 0xFF; - assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 0, false).is_err()); + assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 0, false, &[]).is_err()); // corrupt data let mut bad_data = encrypt_data.clone(); bad_data[0] ^= 0xFF; - assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 0, false).is_err()); + assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 0, false, &[]).is_err()); // wrong key let mut bad_key = key.clone(); bad_key.expose_secret_mut()[0] ^= 0xFF; let bad_cipher_cha = XChaCha20Poly1305::new_from_slice(bad_key.expose_secret()).unwrap(); let mut bad_data = encrypt_data.clone(); - assert!(Decryption::cha_decrypt_buffer(&bad_cipher_cha, &mut bad_data, 0, false).is_err()); + assert!(Decryption::cha_decrypt_buffer(&bad_cipher_cha, &mut bad_data, 0, false, &[]).is_err()); // wrong final chunk flag let mut bad_data = encrypt_data.clone(); - assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 0, true).is_err()); + assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 0, true, &[]).is_err()); // wrong chunk count let mut bad_data = encrypt_data.clone(); - assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 1, false).is_err()); + assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 1, false, &[]).is_err()); + + // wrong associated data + let mut bad_data = encrypt_data.clone(); + assert!(Decryption::cha_decrypt_buffer(&cipher_cha, &mut bad_data, 0, false, &[0xFF]).is_err()); + + // variable values for chunk count and final chunk flag + let mut encrypt_data = data.to_vec(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, true, &[]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, true, &[]).unwrap(); + assert_eq!(data, decrypt_data[..]); + + let mut encrypt_data = data.to_vec(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 42, false, &[]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 42, false, &[]).unwrap(); + assert_eq!(data, decrypt_data[..]); - // different values for chunk count and final chunk flag let mut encrypt_data = data.to_vec(); - Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, true).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0x1000_0000, true, &[]).unwrap(); let mut decrypt_data = encrypt_data.clone(); - Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, true).unwrap(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0x1000_0000, true, &[]).unwrap(); assert_eq!(data, decrypt_data[..]); let mut encrypt_data = data.to_vec(); - Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 42, false).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0xFFFF_FFFF, false, &[]).unwrap(); let mut decrypt_data = encrypt_data.clone(); - Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 42, false).unwrap(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0xFFFF_FFFF, false, &[]).unwrap(); assert_eq!(data, decrypt_data[..]); + // variable values for associated data let mut encrypt_data = data.to_vec(); - Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0x1000_0000, true).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, false, &[1,2,3]).unwrap(); let mut decrypt_data = encrypt_data.clone(); - Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0x1000_0000, true).unwrap(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, false, &[1,2,3]).unwrap(); assert_eq!(data, decrypt_data[..]); let mut encrypt_data = data.to_vec(); - Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0xFFFF_FFFF, false).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, false, &[0u8; 33]).unwrap(); let mut decrypt_data = encrypt_data.clone(); - Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0xFFFF_FFFF, false).unwrap(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, false, &[0u8; 33]).unwrap(); assert_eq!(data, decrypt_data[..]); // empty input data let mut encrypt_data = Vec::new(); - Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, false).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, false, &[]).unwrap(); assert_eq!(encrypt_data.len(), CHA_NONCE_SIZE + CHA_TAG_SIZE); let mut decrypt_data = encrypt_data.clone(); - Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, false).unwrap(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, false, &[]).unwrap(); assert_eq!(decrypt_data.len(), 0); // large input data let data_big = vec![0u8; CHUNK_SIZE * 2 + 123]; let mut encrypt_data = data_big.clone(); - Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, false).unwrap(); + Encryption::cha_encrypt_buffer(&cipher_cha, &mut encrypt_data, 0, false, &[]).unwrap(); let mut decrypt_data = encrypt_data.clone(); - Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, false).unwrap(); + Decryption::cha_decrypt_buffer(&cipher_cha, &mut decrypt_data, 0, false, &[]).unwrap(); assert_eq!(data_big, decrypt_data[..]); } @@ -672,16 +786,16 @@ mod tests { let data: [u8; 100] = rand::random(); let mut encrypt_data = data.to_vec(); - Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data).unwrap(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 0, false, &[]).unwrap(); assert_eq!(encrypt_data.len(), data.len() + AES_NONCE_SIZE + AES_TAG_SIZE); let mut decrypt_data = encrypt_data.clone(); - Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data).unwrap(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 0, false, &[]).unwrap(); assert_eq!(data, decrypt_data[..]); // second encryption must produce different output of the same input let mut encrypt_data2 = data.to_vec(); - Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data2).unwrap(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data2, 0, false, &[]).unwrap(); // nonce part (at the end of the buffer) must be different assert_ne!( encrypt_data[encrypt_data.len() - AES_NONCE_SIZE..], @@ -697,19 +811,87 @@ mod tests { let mut bad_data = encrypt_data.clone(); let last_idx = bad_data.len() - 1; bad_data[last_idx] ^= 0xFF; - assert!(Decryption::aes_decrypt_buffer(&cipher_aes, &mut bad_data).is_err()); + assert!(Decryption::aes_decrypt_buffer(&cipher_aes, &mut bad_data, 0, false, &[]).is_err()); // corrupt data let mut bad_data = encrypt_data.clone(); bad_data[0] ^= 0xFF; - assert!(Decryption::aes_decrypt_buffer(&cipher_aes, &mut bad_data).is_err()); + assert!(Decryption::aes_decrypt_buffer(&cipher_aes, &mut bad_data, 0, false, &[]).is_err()); // wrong key let mut bad_key = key.clone(); bad_key.expose_secret_mut()[0] ^= 0xFF; let bad_cipher_aes = Aes256GcmSiv::new_from_slice(bad_key.expose_secret()).unwrap(); let mut bad_data = encrypt_data.clone(); - assert!(Decryption::aes_decrypt_buffer(&bad_cipher_aes, &mut bad_data).is_err()); + assert!(Decryption::aes_decrypt_buffer(&bad_cipher_aes, &mut bad_data, 0, false, &[]).is_err()); + + + // wrong final chunk flag + let mut bad_data = encrypt_data.clone(); + assert!(Decryption::aes_decrypt_buffer(&cipher_aes, &mut bad_data, 0, true, &[]).is_err()); + + // wrong chunk count + let mut bad_data = encrypt_data.clone(); + assert!(Decryption::aes_decrypt_buffer(&cipher_aes, &mut bad_data, 1, false, &[]).is_err()); + + // wrong associated data + let mut bad_data = encrypt_data.clone(); + assert!(Decryption::aes_decrypt_buffer(&cipher_aes, &mut bad_data, 0, false, &[0xFF]).is_err()); + + // variable values for chunk count and final chunk flag + let mut encrypt_data = data.to_vec(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 0, true, &[]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 0, true, &[]).unwrap(); + assert_eq!(data, decrypt_data[..]); + + let mut encrypt_data = data.to_vec(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 42, false, &[]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 42, false, &[]).unwrap(); + assert_eq!(data, decrypt_data[..]); + + let mut encrypt_data = data.to_vec(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 0x1000_0000, true, &[]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 0x1000_0000, true, &[]).unwrap(); + assert_eq!(data, decrypt_data[..]); + + let mut encrypt_data = data.to_vec(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 0xFFFF_FFFF, false, &[]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 0xFFFF_FFFF, false, &[]).unwrap(); + assert_eq!(data, decrypt_data[..]); + + // variable values for associated data + let mut encrypt_data = data.to_vec(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 0, false, &[1,2,3]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 0, false, &[1,2,3]).unwrap(); + assert_eq!(data, decrypt_data[..]); + + let mut encrypt_data = data.to_vec(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 0, false, &[0u8; 33]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 0, false, &[0u8; 33]).unwrap(); + assert_eq!(data, decrypt_data[..]); + + // empty input data + let mut encrypt_data = Vec::new(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 0, false, &[]).unwrap(); + assert_eq!(encrypt_data.len(), AES_NONCE_SIZE + AES_TAG_SIZE); + let mut decrypt_data = encrypt_data.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 0, false, &[]).unwrap(); + assert_eq!(decrypt_data.len(), 0); + + // large input data + let data_big = vec![0u8; CHUNK_SIZE * 2 + 123]; + let mut encrypt_data = data_big.clone(); + Encryption::aes_encrypt_buffer(&cipher_aes, &mut encrypt_data, 0, false, &[]).unwrap(); + let mut decrypt_data = encrypt_data.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut decrypt_data, 0, false, &[]).unwrap(); + assert_eq!(data_big, decrypt_data[..]); + } #[test] @@ -814,6 +996,288 @@ mod tests { assert_eq!(rcount3, 3); } + #[test] + fn test_set_paths() { + // Create test files and directories + let mut test_dir = PathBuf::from("test_set_paths_dir"); + let _ = fs::remove_dir_all(&test_dir); + fs::create_dir_all(&test_dir).unwrap(); + test_dir = test_dir.canonicalize().unwrap(); + + let test_file = test_dir.join("test_file.txt"); + fs::write(&test_file, b"test data").unwrap(); + + let sub_dir = test_dir.join("sub_dir"); + fs::create_dir_all(&sub_dir).unwrap(); + + let output_dir = test_dir.join("output_dir"); + fs::create_dir_all(&output_dir).unwrap(); + + // 1. File input, no output dir, no split + { + let (file_out, archive, strip_dir) = Encryption::set_paths(&test_file, &test_dir, None, true).unwrap(); + assert!(!archive); + assert_eq!(strip_dir, None); + let mut expected = test_file.clone(); + expected.add_extension(ENCRYPTED_FILE_EXT); + assert_eq!(file_out, path::absolute(expected).unwrap()); + } + + // 2. File input, no output dir, with split + { + let (file_out, archive, strip_dir) = Encryption::set_paths(&test_file, &test_dir, None, false).unwrap(); + assert!(!archive); + assert_eq!(strip_dir, None); + let mut expected = test_file.clone(); + expected.add_extension(SPLIT_ENC_FILE_EXT); + assert_eq!(file_out, path::absolute(expected).unwrap()); + } + + // 3. File input, with output dir, no split + { + let (file_out, archive, strip_dir) = Encryption::set_paths(&test_file, &test_dir, Some(&output_dir), true).unwrap(); + assert!(!archive); + assert_eq!(strip_dir, None); + let mut expected = test_file.clone(); + expected.add_extension(ENCRYPTED_FILE_EXT); + let expected_out = path::absolute(output_dir.join(expected.file_name().unwrap())).unwrap(); + assert_eq!(file_out, expected_out); + } + + // 4. Directory input (sub_dir), no output dir, no split + { + let (file_out, archive, strip_dir) = Encryption::set_paths(&sub_dir, &test_dir, None, true).unwrap(); + assert!(archive); + assert_eq!(strip_dir, Some(test_dir.clone())); + let mut expected = sub_dir.clone(); + expected.add_extension(ENCRYPTED_FILE_EXT); + assert_eq!(file_out, path::absolute(expected).unwrap()); + } + + // 5. Directory input (sub_dir), with output dir, with split + { + let (file_out, archive, strip_dir) = Encryption::set_paths(&sub_dir, &test_dir, Some(&output_dir), false).unwrap(); + assert!(archive); + assert_eq!(strip_dir, Some(test_dir.clone())); + let mut expected = PathBuf::from("sub_dir"); + expected.add_extension(SPLIT_ENC_FILE_EXT); + let expected_out = path::absolute(output_dir.join(expected.file_name().unwrap())).unwrap(); + assert_eq!(file_out, expected_out); + } + + // 6. Directory input without parent/file_name (e.g. "/") + { + let root_path = + if cfg!(unix) { + PathBuf::from("/") + } else { + PathBuf::from("c:/") + }; + let (file_out, archive, strip_dir) = Encryption::set_paths(&root_path, &test_dir, None, true).unwrap(); + assert!(archive); + assert_eq!(strip_dir, Some(root_path.clone())); + let mut expected = test_dir.join("archive"); + expected.add_extension(ENCRYPTED_FILE_EXT); + assert_eq!(file_out, expected); + } + + // Clean up + let _ = fs::remove_dir_all(&test_dir); + } + + #[test] + fn test_header() { + let key_cha = [0x11u8; 32]; + let key_aes = [0x22u8; 32]; + let cipher_cha = XChaCha20Poly1305::new_from_slice(&key_cha).unwrap(); + let cipher_aes = Aes256GcmSiv::new_from_slice(&key_aes).unwrap(); + let salt_pw = [1u8; SALT_SIZE]; + let salt_enc = [2u8; SALT_SIZE]; + let salt_body_pw = [3u8; SALT_SIZE]; + let salt_body_enc = [4u8; SALT_SIZE]; + let salt_fido = [5u8; SALT_SIZE]; + let file_id = [0xa5; FILE_ID_SIZE]; + + // 1. Test permutations of boolean flags without USB key + let flag_combinations = [ + (false, false, false), + (true, false, false), + (false, true, false), + (false, false, true), + (true, true, false), + (true, false, true), + (false, true, true), + (true, true, true), + ]; + + for (compress, archive, keyfile) in flag_combinations { + let enc_params = EncHeaderParams { + salt_body_pw: &salt_body_pw, + salt_body_enc: &salt_body_enc, + file_id: &file_id, + salt_body_fido: None, + credential_id_fido: None, + compress, + archive, + keyfile, + usb_key: false, + }; + + let mut header = Encryption::create_header( + &salt_pw, + &salt_enc, + enc_params, + &cipher_cha, + &cipher_aes, + ).unwrap(); + + // Check unencrypted header part + let (extracted_salt_pw, extracted_salt_enc) = Decryption::get_unencrypted_header_items(&mut header); + assert_eq!(extracted_salt_pw, salt_pw); + assert_eq!(extracted_salt_enc, salt_enc); + + // Decrypt and evaluate encrypted header part + let (version, params) = Decryption::get_encrypted_header_items( + &mut header, + &cipher_cha, + &cipher_aes + ).unwrap(); + + assert_eq!(version, FILE_FORMAT_VERSION); + assert_eq!(params.compress, compress); + assert_eq!(params.archive, archive); + assert_eq!(params.keyfile, keyfile); + assert!(!params.usb_key); + assert_eq!(params.salt_body_pw, &salt_body_pw[..]); + assert_eq!(params.salt_body_enc, &salt_body_enc[..]); + assert_eq!(params.file_id, &file_id[..]); + assert_eq!(params.salt_body_fido, None); + assert_eq!(params.credential_id_fido, None); + } + + // 2. Test USB key / FIDO enabled scenarios with varying credential ID lengths + let cred_ids = [ + vec![], // empty credential ID + vec![42u8], // 1 byte + vec![0xAA; 64], // 64 bytes + vec![0xBB; 255], // 255 bytes (maximum u8 length) + ]; + + for cred_id in cred_ids { + let enc_params = EncHeaderParams { + salt_body_pw: &salt_body_pw, + salt_body_enc: &salt_body_enc, + file_id: &file_id, + salt_body_fido: Some(&salt_fido), + credential_id_fido: Some(cred_id.clone()), + compress: true, + archive: false, + keyfile: true, + usb_key: true, + }; + + let mut header = Encryption::create_header( + &salt_pw, + &salt_enc, + enc_params, + &cipher_cha, + &cipher_aes, + ).unwrap(); + + let _ = Decryption::get_unencrypted_header_items(&mut header); + let (version, params) = Decryption::get_encrypted_header_items( + &mut header, + &cipher_cha, + &cipher_aes, + ).unwrap(); + + assert_eq!(version, FILE_FORMAT_VERSION); + assert!(params.compress); + assert!(!params.archive); + assert!(params.keyfile); + assert!(params.usb_key); + assert_eq!(params.salt_body_pw, &salt_body_pw[..]); + assert_eq!(params.salt_body_enc, &salt_body_enc[..]); + assert_eq!(params.file_id, &file_id[..]); + assert_eq!(params.salt_body_fido, Some(&salt_fido[..])); + assert_eq!(params.credential_id_fido, Some(cred_id)); + } + + // 4. Test error handling + let valid_enc_params = EncHeaderParams { + salt_body_pw: &salt_body_pw, + salt_body_enc: &salt_body_enc, + file_id: &file_id, + salt_body_fido: None, + credential_id_fido: None, + compress: false, + archive: false, + keyfile: false, + usb_key: false, + }; + + let mut valid_header = Encryption::create_header( + &salt_pw, + &salt_enc, + valid_enc_params, + &cipher_cha, + &cipher_aes, + ).unwrap(); + + let _ = Decryption::get_unencrypted_header_items(&mut valid_header); + let valid_enc_header = valid_header; + + // 4a. Wrong AES cipher + let wrong_key_aes = [0x99u8; 32]; + let wrong_cipher_aes = Aes256GcmSiv::new_from_slice(&wrong_key_aes).unwrap(); + let mut header = valid_enc_header.clone(); + assert!(Decryption::get_encrypted_header_items( + &mut header, + &cipher_cha, + &wrong_cipher_aes, + ).is_err()); + + // 4b. Wrong ChaCha cipher + let wrong_key_cha = [0x88u8; 32]; + let wrong_cipher_cha = XChaCha20Poly1305::new_from_slice(&wrong_key_cha).unwrap(); + let mut header = valid_enc_header.clone(); + assert!(Decryption::get_encrypted_header_items( + &mut header, + &wrong_cipher_cha, + &cipher_aes, + ).is_err()); + + // 4c. ChaCha authentication failure (AES decrypts, but ChaCha tag check fails) + let mut bad_cha_header = valid_enc_header.clone(); + Decryption::aes_decrypt_buffer(&cipher_aes, &mut bad_cha_header, u32::MAX, true, "header_aes".as_bytes()).unwrap(); + bad_cha_header[0] ^= 0x01; // corrupt ChaCha ciphertext + Encryption::aes_encrypt_buffer(&cipher_aes, &mut bad_cha_header, u32::MAX, true, "header_aes".as_bytes()).unwrap(); + assert!(Decryption::get_encrypted_header_items( + &mut bad_cha_header, + &cipher_cha, + &cipher_aes, + ).is_err()); + + // 4d. Corrupted AES ciphertext byte + let mut corrupted_header = valid_enc_header.clone(); + corrupted_header[0] ^= 0x01; + assert!(Decryption::get_encrypted_header_items( + &mut corrupted_header, + &cipher_cha, + &cipher_aes, + ).is_err()); + + // 4e. Corrupted AES tag byte + let mut corrupted_tag = valid_enc_header.clone(); + let last_idx = corrupted_tag.len() - 1; + corrupted_tag[last_idx] ^= 0x01; + assert!(Decryption::get_encrypted_header_items( + &mut corrupted_tag, + &cipher_cha, + &cipher_aes, + ).is_err()); + } + #[test] fn test_crypt_general() { // create file with random data for encryption @@ -827,7 +1291,7 @@ mod tests { fs::write(&filepath_in, &data).unwrap(); // encrypt, decrypt - Encryption::encrypt(&filepath_in, None, None, false, vec![], false).unwrap(); + Encryption::encrypt(&filepath_in, None, None, false, false, vec![], false).unwrap(); assert!(filepath_out.exists()); // encrypted file must be different than original data assert_ne!(data, fs::read(&filepath_out).unwrap()); @@ -844,14 +1308,14 @@ mod tests { // with compression fs::write(&filepath_in, &data).unwrap(); - Encryption::encrypt(&filepath_in, None, None, true, vec![], false).unwrap(); + Encryption::encrypt(&filepath_in, None, None, false, true, vec![], false).unwrap(); Decryption::decrypt(&filepath_out, None, None, false, false).unwrap(); let decrypt_data = fs::read(&filepath_in).unwrap(); assert_eq!(data, decrypt_data[..]); // encrypt with output directory let out_enc_dir = path::absolute(PathBuf::from("test_cc_enc_dir")).unwrap(); - Encryption::encrypt(&filepath_in, Some(&out_enc_dir), None, false, vec![], false).unwrap(); + Encryption::encrypt(&filepath_in, Some(&out_enc_dir), None, false, false, vec![], false).unwrap(); assert!(&out_enc_dir.join(&filepath_out).exists()); // decrypt with output directory let out_dec_dir = path::absolute(PathBuf::from("test_cc_dec_dir")).unwrap(); @@ -863,9 +1327,22 @@ mod tests { Decryption::decrypt(&out_enc_dir.join(&filepath_out), Some(&out_dec_dir), None, false, true).unwrap(); assert!(&out_dec_dir.join(&filepath_in).exists()); + // empty file + let filepath_in_empty = path::absolute(PathBuf::from("test_cc_empty.bin")).unwrap(); + let mut filepath_out_empty = filepath_in_empty.clone(); + filepath_out_empty.add_extension(ENCRYPTED_FILE_EXT); + fs::write(&filepath_in_empty, vec![]).unwrap(); + Encryption::encrypt(&filepath_in_empty, None, None, false, false, vec![], false).unwrap(); + assert!(filepath_out_empty.exists()); + assert_eq!(fs::metadata(&filepath_out_empty).unwrap().len(), (HEADER_SIZE + CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE) as u64); + Decryption::decrypt(&filepath_out_empty, None, None, false, false).unwrap(); + assert_eq!(fs::metadata(&filepath_in_empty).unwrap().len(), 0); + // cleanup let _ = fs::remove_file(&filepath_in); + let _ = fs::remove_file(&filepath_in_empty); let _ = fs::remove_file(&filepath_out); + let _ = fs::remove_file(&filepath_out_empty); let _ = fs::remove_file(&filepath_kf); let _ = fs::remove_dir_all(&out_enc_dir); let _ = fs::remove_dir_all(&out_dec_dir); @@ -888,7 +1365,7 @@ mod tests { fs::write(&filepath_kf, &data_kf).unwrap(); // use keyfile, encrypt, decrypt - Encryption::encrypt(&filepath_in, None, Some(&filepath_kf), false, vec![], false).unwrap(); + Encryption::encrypt(&filepath_in, None, Some(&filepath_kf), false, false, vec![], false).unwrap(); Decryption::decrypt(&filepath_out, None, Some(&filepath_kf), false, false).unwrap(); // read and compare decrypted file against original data @@ -899,18 +1376,18 @@ mod tests { assert!(Decryption::decrypt(&filepath_out, None, None, false, false).is_err()); // key file does not exist - assert!(Encryption::encrypt(&filepath_in, None, Some(&PathBuf::from("test_miss")), false, vec![], false).is_err()); + assert!(Encryption::encrypt(&filepath_in, None, Some(&PathBuf::from("test_miss")), false, false, vec![], false).is_err()); assert!(Decryption::decrypt(&filepath_out, None, Some(&PathBuf::from("test_miss")), false, false).is_err()); // input file does not exist - assert!(Encryption::encrypt(&path::absolute(PathBuf::from("test_miss")).unwrap(), None, None, false, vec![], false).is_err()); + assert!(Encryption::encrypt(&path::absolute(PathBuf::from("test_miss")).unwrap(), None, None, false, false, vec![], false).is_err()); assert!(Decryption::decrypt(&PathBuf::from("test_miss.cce"), None, None, false, false).is_err()); assert!(!fs::exists("test_miss").unwrap()); assert!(!fs::exists("test_miss.cce").unwrap()); // with compression fs::write(&filepath_in, &data).unwrap(); - Encryption::encrypt(&filepath_in, None, Some(&filepath_kf), true, vec![], false).unwrap(); + Encryption::encrypt(&filepath_in, None, Some(&filepath_kf), false, true, vec![], false).unwrap(); Decryption::decrypt(&filepath_out, None, Some(&filepath_kf), false, false).unwrap(); let decrypt_data = fs::read(&filepath_in).unwrap(); assert_eq!(data, decrypt_data[..]); @@ -933,7 +1410,7 @@ mod tests { fs::write(&filepath_in, &data).unwrap(); // encrypt and split output - Encryption::encrypt(&filepath_in, None, None, false, vec![1048576, 12], false).unwrap(); + Encryption::encrypt(&filepath_in, None, None, false, false, vec![1048576, 12], false).unwrap(); // concatenate spilt output files let mut data_concat = fs::read("test_cc_split.bin.c00").unwrap(); @@ -955,7 +1432,7 @@ mod tests { assert_eq!(data, decrypt_data[..]); // with compression - Encryption::encrypt(&filepath_in, None, None, true, vec![11, 12, 1024*100], false).unwrap(); + Encryption::encrypt(&filepath_in, None, None, false, true, vec![11, 12, 1024*100], false).unwrap(); Decryption::decrypt(&PathBuf::from("test_cc_split.bin.c00"), None, None, false, false).unwrap(); let decrypt_data = fs::read(&filepath_in).unwrap(); assert_eq!(data, decrypt_data[..]); @@ -1006,7 +1483,7 @@ mod tests { fs::hard_link(&file2, &hardlink2).unwrap(); // Build archive of directory and encrypt it - Encryption::encrypt(&dir_path.canonicalize().unwrap(), None, None, false, vec![], false).unwrap(); + Encryption::encrypt(&dir_path.canonicalize().unwrap(), None, None, false, false, vec![], false).unwrap(); // Delete the original files before extracting to verify recreation fs::remove_dir_all(&dir_path).unwrap(); @@ -1045,7 +1522,7 @@ mod tests { // With output directory, with split, check exclusion of output archive files let out_enc_dir = &dir_path; - Encryption::encrypt(&dir_path.canonicalize().unwrap(), Some(&out_enc_dir.canonicalize().unwrap()), None, false, vec![1000,10000], false).unwrap(); + Encryption::encrypt(&dir_path.canonicalize().unwrap(), Some(&out_enc_dir.canonicalize().unwrap()), None, false, false, vec![1000,10000], false).unwrap(); let archive_path2 = out_enc_dir.join(&dir_path).with_extension(SPLIT_ENC_FILE_EXT); assert!(archive_path2.exists()); @@ -1080,91 +1557,27 @@ mod tests { } #[test] - fn test_set_paths() { - // Create test files and directories - let mut test_dir = PathBuf::from("test_set_paths_dir"); - let _ = fs::remove_dir_all(&test_dir); - fs::create_dir_all(&test_dir).unwrap(); - test_dir = test_dir.canonicalize().unwrap(); - - let test_file = test_dir.join("test_file.txt"); - fs::write(&test_file, b"test data").unwrap(); - - let sub_dir = test_dir.join("sub_dir"); - fs::create_dir_all(&sub_dir).unwrap(); - - let output_dir = test_dir.join("output_dir"); - fs::create_dir_all(&output_dir).unwrap(); - - // 1. File input, no output dir, no split - { - let (file_out, archive, strip_dir) = Encryption::set_paths(&test_file, &test_dir, None, true).unwrap(); - assert!(!archive); - assert_eq!(strip_dir, None); - let mut expected = test_file.clone(); - expected.add_extension(ENCRYPTED_FILE_EXT); - assert_eq!(file_out, path::absolute(expected).unwrap()); - } - - // 2. File input, no output dir, with split - { - let (file_out, archive, strip_dir) = Encryption::set_paths(&test_file, &test_dir, None, false).unwrap(); - assert!(!archive); - assert_eq!(strip_dir, None); - let mut expected = test_file.clone(); - expected.add_extension(SPLIT_ENC_FILE_EXT); - assert_eq!(file_out, path::absolute(expected).unwrap()); - } - - // 3. File input, with output dir, no split - { - let (file_out, archive, strip_dir) = Encryption::set_paths(&test_file, &test_dir, Some(&output_dir), true).unwrap(); - assert!(!archive); - assert_eq!(strip_dir, None); - let mut expected = test_file.clone(); - expected.add_extension(ENCRYPTED_FILE_EXT); - let expected_out = path::absolute(output_dir.join(expected.file_name().unwrap())).unwrap(); - assert_eq!(file_out, expected_out); - } + #[ignore = "requires USB key and user interaction"] + // Test requires PIN entry for USB security key and touching the key + // cargo t test_crypt_with_usbkey -- --include-ignored --nocapture + fn test_crypt_with_usbkey() { + let filepath_in = path::absolute(PathBuf::from("test_cc_usbkey.bin")).unwrap(); + let mut filepath_out = filepath_in.clone(); + filepath_out.add_extension(ENCRYPTED_FILE_EXT); - // 4. Directory input (sub_dir), no output dir, no split - { - let (file_out, archive, strip_dir) = Encryption::set_paths(&sub_dir, &test_dir, None, true).unwrap(); - assert!(archive); - assert_eq!(strip_dir, Some(test_dir.clone())); - let mut expected = sub_dir.clone(); - expected.add_extension(ENCRYPTED_FILE_EXT); - assert_eq!(file_out, path::absolute(expected).unwrap()); - } + let mut data = vec![0; 1024 * 1024]; + rand::rng().fill_bytes(&mut data); + fs::write(&filepath_in, &data).unwrap(); - // 5. Directory input (sub_dir), with output dir, with split - { - let (file_out, archive, strip_dir) = Encryption::set_paths(&sub_dir, &test_dir, Some(&output_dir), false).unwrap(); - assert!(archive); - assert_eq!(strip_dir, Some(test_dir.clone())); - let mut expected = PathBuf::from("sub_dir"); - expected.add_extension(SPLIT_ENC_FILE_EXT); - let expected_out = path::absolute(output_dir.join(expected.file_name().unwrap())).unwrap(); - assert_eq!(file_out, expected_out); - } + println!("=== Encryption ==="); + Encryption::encrypt(&filepath_in, None, None, true, false, vec![], false).unwrap(); + println!("=== Decryption ==="); + Decryption::decrypt(&filepath_out, None, None, false, false).unwrap(); - // 6. Directory input without parent/file_name (e.g. "/") - { - let root_path = - if cfg!(unix) { - PathBuf::from("/") - } else { - PathBuf::from("c:/") - }; - let (file_out, archive, strip_dir) = Encryption::set_paths(&root_path, &test_dir, None, true).unwrap(); - assert!(archive); - assert_eq!(strip_dir, Some(root_path.clone())); - let mut expected = test_dir.join("archive"); - expected.add_extension(ENCRYPTED_FILE_EXT); - assert_eq!(file_out, expected); - } + let decrypt_data = fs::read(&filepath_in).unwrap(); + assert_eq!(data, decrypt_data[..]); - // Clean up - let _ = fs::remove_dir_all(&test_dir); + let _ = fs::remove_file(&filepath_in); + let _ = fs::remove_file(&filepath_out); } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8b597a3..3acbc60 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,18 +11,20 @@ pub mod decryption; pub mod archive; -pub const FILE_FORMAT_VERSION: u8 = 6; +pub const FILE_FORMAT_VERSION: u8 = 7; pub const ENCRYPTED_FILE_EXT: &str = "cce"; pub const SPLIT_ENC_FILE_EXT: &str = "c00"; pub const CHUNK_SIZE: usize = 1_048_576; // 1024 * 1024 bytes pub const MAX_KEYFILE_CHUNKS: usize = 64; pub const SALT_SIZE: usize = 32; pub const KEY_SIZE: usize = 32; +pub const FILE_ID_SIZE: usize = 32; pub const COMPRESS_LENGTH_SIZE: usize = 3; pub const AES_NONCE_SIZE: usize = ::NonceSize::USIZE; // 12 bytes pub const CHA_NONCE_SIZE: usize = ::NonceSize::USIZE; // 24 bytes pub const AES_TAG_SIZE: usize = ::TagSize::USIZE; // 16 bytes pub const CHA_TAG_SIZE: usize = ::TagSize::USIZE; // 16 bytes -pub const HEADER_SIZE: usize = 3 * SALT_SIZE + 4 + CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE; +pub const MAX_CREDENTIAL_ID_FIDO_SIZE: usize = 256; +pub const HEADER_SIZE: usize = 5 * SALT_SIZE + FILE_ID_SIZE + 3 + MAX_CREDENTIAL_ID_FIDO_SIZE + CHA_NONCE_SIZE + CHA_TAG_SIZE + AES_NONCE_SIZE + AES_TAG_SIZE; pub type Result = std::result::Result>; diff --git a/src/main.rs b/src/main.rs index 8dc47cc..538dc8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,10 +14,6 @@ use cryptcrypt::Result; /// With option -s the encrypted output is split into files with extensions .c00, .c01, .c02, ... /// If a file ending in .c00 is decrypted, the whole split series will be read. struct Args { - /// Output directory, it is created if it does not exist - #[arg(short, long)] - out_dir: Option, - /// Decrypt file (with extension '.cce' or for split series '.c00') #[arg(short, long, default_value_t = false)] decrypt: bool, @@ -26,23 +22,31 @@ struct Args { #[arg(short, long)] keyfile: Option, - /// Compress data before encryption, automatically detected on decryption - #[arg(short = 'z', long, default_value_t = false)] - compress: bool, + #[arg(short, long, default_value_t = false)] + /// List elements of an archive file, do not create its elements + list_archive: bool, + + /// Output directory, it is created if it does not exist + #[arg(short, long)] + out_dir: Option, /// Split encrypted data into pieces of binary byte sizes (e.g. 2g,3g,1g) [G|g|M|m|K|k] #[arg(short, long, value_delimiter = ',', value_parser = |s: &str| { let cfg = Config::new().with_binary(); cfg.parse_size(s) })] split: Vec, - #[arg(short, long, default_value_t = false)] - /// List elements of an archive file, do not create its elements - list_archive: bool, - + /// Additional USB security key/FIDO authenticator to supplement the password, automatically detected on decryption + #[arg(short, long)] + usb_key: bool, + /// Show details about operations #[arg(short, long, default_value_t = false)] verbose: bool, + /// Compress data before encryption, automatically detected on decryption + #[arg(short = 'z', long, default_value_t = false)] + compress: bool, + /// File that should be encrypted or decrypted. /// If a directory is given, its contents is archived and encrypted. file_or_directory: PathBuf, @@ -79,7 +83,7 @@ fn run() -> Result<()> { if args.decrypt { Decryption::decrypt(&filepath, output_dir.as_ref(), keyfilepath.as_ref(), args.verbose, args.list_archive)?; } else { - Encryption::encrypt(&filepath, output_dir.as_ref(), keyfilepath.as_ref(), args.compress, args.split, args.verbose)?; + Encryption::encrypt(&filepath, output_dir.as_ref(), keyfilepath.as_ref(), args.usb_key, args.compress, args.split, args.verbose)?; } Ok(())