diff --git a/.gitignore b/.gitignore index e9f6b34..ff2563a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target .requirements -Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..052aa7f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1457 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "assert_cmd" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +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 = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jcz" +version = "0.2.2" +dependencies = [ + "argon2", + "assert_cmd", + "chrono", + "clap", + "env_logger", + "log", + "pem", + "predicates", + "proptest", + "rand 0.8.5", + "rayon", + "ring", + "rpassword", + "rsa", + "sha2", + "tempfile", + "zeroize", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "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", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpassword" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.59.0", +] + +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rtoolbox" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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", +] + +[[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", +] + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +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 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[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_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[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_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/Cargo.toml b/Cargo.toml index 106f0bb..db141f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jcz" -version = "0.2.2" +version = "0.2.3" edition = "2021" authors = ["JCZ Contributors"] description = "Just Compress Zip - A unified compression utility" @@ -24,9 +24,21 @@ chrono = "0.4" # Temporary directory management tempfile = "3.8" +# Cryptography +ring = "0.17" +rsa = "0.9" +argon2 = "0.5" +pem = "3.0" +zeroize = "1.7" +sha2 = "0.10" +rand = "0.8" +rpassword = "7.3" + [dev-dependencies] assert_cmd = "2.0" predicates = "3.0" +proptest = "1.4" +rand = "0.8" [[bin]] name = "jcz" diff --git a/README.md b/README.md index 751103f..6993086 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ A unified command-line compression utility written in Rust, providing a consiste ## Features - **Multi-Format Support**: GZIP, BZIP2, XZ, ZIP, TAR, and compound formats (TGZ, TBZ2, TXZ) +- **File Encryption**: Password-based and RSA public-key encryption for compressed files - **Parallel Processing**: Concurrent compression/decompression of multiple files using Rayon - **Timestamp Options**: Add timestamps to output filenames (date, datetime, or nanoseconds) - **File Collection**: Combine multiple files into single archives @@ -62,6 +63,28 @@ jcz -d archive.tar.gz jcz -d file1.gz file2.bz2 file3.xz ``` +### Encryption + +```bash +# Encrypt with password +jcz -c gzip -e file.txt +# Output: file.txt.gz.jcze (encrypted) + +# Encrypt with RSA public key +jcz -c gzip --encrypt-key public.pem file.txt +# Output: file.txt.gz.jcze (encrypted) + +# Decrypt password-encrypted file (will prompt for password) +jcz -d file.txt.gz.jcze + +# Decrypt RSA-encrypted file with private key +jcz -d --decrypt-key private.pem file.txt.gz.jcze + +# Encrypt compound format +jcz -c tgz -e directory/ +# Output: directory.tar.gz.jcze +``` + ### Advanced Features ```bash @@ -77,6 +100,10 @@ jcz -c tgz -a myarchive file1.txt file2.txt dir/ # Collect files without parent directory wrapper jcz -c tgz -A myarchive file1.txt file2.txt + +# Combine encryption with other options +jcz -c gzip -e -t 2 -C /secure/ file.txt +# Output: /secure/file.txt_20251201_143022.gz.jcze ``` ### Options @@ -89,6 +116,10 @@ jcz -c tgz -A myarchive file1.txt file2.txt -a, --collect Collect files into archive (with parent directory) -A, --collect-flat Collect files into archive (flat, without parent directory) -t, --timestamp Timestamp option: 0=none, 1=date, 2=datetime, 3=nanoseconds [default: 0] +-e, --encrypt-password Enable password-based encryption + --encrypt-key RSA public key file for encryption + --decrypt-key RSA private key file for decryption +-f, --force Force overwrite without prompting -h, --help Print help -V, --version Print version ``` @@ -122,10 +153,26 @@ The implementation follows a modular design: - **Core Module**: Trait definitions, error types, configuration structures - **Compressor Modules**: Individual implementations for GZIP, BZIP2, XZ, ZIP, TAR -- **Operations Module**: High-level operations (compress, decompress, compound, collection) +- **Crypto Module**: Encryption/decryption with password and RSA support +- **Operations Module**: High-level operations (compress, decompress, encrypt, decrypt, compound, collection) - **Utils Module**: File system utilities, logging, validation, timestamp generation - **CLI Module**: Command-line argument parsing and command execution +## Encryption Details + +### Password-Based Encryption +- **Algorithm**: AES-256-GCM (authenticated encryption) +- **Key Derivation**: Argon2id with secure parameters (64MB memory, 3 iterations) +- **Security**: Cryptographically secure random salt and nonce generation +- **File Extension**: `.jcze` (JCZ Encrypted) + +### RSA Encryption +- **Algorithm**: RSA with OAEP-SHA256 padding +- **Key Size**: Minimum 2048 bits (4096 bits recommended) +- **Hybrid Approach**: RSA encrypts a random AES-256 symmetric key, which encrypts the data +- **Key Format**: PEM-encoded public/private keys +- **Security**: Public key encrypts, private key decrypts (standard RSA confidentiality) + ## Design Highlights - **Trait-based polymorphism**: All compressors implement the `Compressor` trait @@ -140,6 +187,10 @@ The implementation follows a modular design: - `rayon` - Data parallelism - `log` / `env_logger` - Logging infrastructure - `chrono` - Timestamp generation +- `ring` - AES-256-GCM encryption +- `rsa` - RSA public-key cryptography +- `argon2` - Password-based key derivation +- `rpassword` - Secure password input ## System Requirements diff --git a/docs/jcz_sdd.md b/docs/jcz_sdd.md index e0aa481..063d956 100644 --- a/docs/jcz_sdd.md +++ b/docs/jcz_sdd.md @@ -1,8 +1,8 @@ # Software Design Document (SDD) ## JCZ - Just Compress Zip Utility (Rust Implementation) -**Version:** 1.1 -**Date:** 2025-11-30 +**Version:** 1.2 +**Date:** 2025-12-01 **Document Status:** Final **Implementation Language:** Rust @@ -2479,7 +2479,249 @@ jobs: --- -## 11. Appendices +## 11. Encryption Module Design + +### 11.1 Overview + +The encryption module provides secure file encryption capabilities using both password-based and RSA public-key cryptography. It operates on compressed files, wrapping them in an encrypted container format. + +### 11.2 Module Structure + +``` +src/crypto/ +├── mod.rs # Public API and core types +├── container.rs # Encrypted container format +├── password.rs # Password-based encryption +├── rsa.rs # RSA encryption +└── keys.rs # Key management utilities +``` + +### 11.3 Encrypted Container Format + +``` +[Magic Bytes: 4 bytes] "JCZE" (0x4A 0x43 0x5A 0x45) +[Version: 1 byte] +[Encryption Type: 1 byte] (0x01 = Password, 0x02 = RSA) +[Metadata Length: 4 bytes] (little-endian) +[Metadata: variable] +[Encrypted Data: variable] +``` + +**Password Metadata Format:** +``` +[Salt: 32 bytes] +[Nonce: 12 bytes] +[Argon2 Memory Cost: 4 bytes] +[Argon2 Time Cost: 4 bytes] +[Argon2 Parallelism: 4 bytes] +``` + +**RSA Metadata Format:** +``` +[Encrypted Key Length: 4 bytes] +[Encrypted Symmetric Key: variable] +[Nonce: 12 bytes] +``` + +### 11.4 Core Types + +```rust +pub enum EncryptionMethod { + Password, + Rsa { public_key_path: PathBuf }, +} + +pub enum DecryptionMethod { + Password, + Rsa { private_key_path: PathBuf }, +} + +pub enum EncryptionType { + Password = 0x01, + Rsa = 0x02, +} + +pub enum EncryptionMetadata { + Password { + salt: [u8; 32], + nonce: [u8; 12], + argon2_params: Argon2Params, + }, + Rsa { + encrypted_key: Vec, + nonce: [u8; 12], + }, +} + +pub struct Argon2Params { + pub memory_cost: u32, // Default: 65536 (64 MB) + pub time_cost: u32, // Default: 3 + pub parallelism: u32, // Default: 4 +} +``` + +### 11.5 Password-Based Encryption + +**Algorithm**: AES-256-GCM with Argon2id key derivation + +**Process**: +1. Prompt user for password (no echo) +2. Generate 32-byte random salt using `ring::rand::SystemRandom` +3. Derive 256-bit key using Argon2id +4. Generate 12-byte random nonce +5. Encrypt data using AES-256-GCM +6. Create container with salt, nonce, and Argon2 parameters +7. Write to `.jcze` file + +**Security Properties**: +- Argon2id provides resistance to GPU/ASIC attacks +- AES-256-GCM provides authenticated encryption +- Random salt prevents rainbow table attacks +- Unique nonce per encryption prevents replay attacks + +### 11.6 RSA Encryption + +**Algorithm**: Hybrid encryption with RSA-OAEP-SHA256 and AES-256-GCM + +**Process**: +1. Parse RSA public key from PEM file +2. Validate key size (minimum 2048 bits) +3. Generate random 256-bit AES symmetric key +4. Generate 12-byte random nonce +5. Encrypt data using AES-256-GCM with symmetric key +6. Encrypt symmetric key using RSA-OAEP-SHA256 with public key +7. Create container with encrypted key and nonce +8. Write to `.jcze` file + +**Security Properties**: +- RSA-OAEP provides secure key encapsulation +- Hybrid approach allows efficient encryption of large files +- Public key encryption ensures only private key holder can decrypt +- Minimum 2048-bit keys provide adequate security + +### 11.7 Decryption + +**Password Decryption**: +1. Read encrypted container +2. Parse metadata to extract salt, nonce, and Argon2 parameters +3. Prompt user for password +4. Derive key using stored parameters +5. Decrypt using AES-256-GCM +6. Verify authentication tag +7. Write decrypted data + +**RSA Decryption**: +1. Read encrypted container +2. Parse RSA private key from PEM file +3. Decrypt symmetric key using RSA-OAEP-SHA256 +4. Decrypt data using AES-256-GCM with recovered key +5. Verify authentication tag +6. Write decrypted data + +### 11.8 Integration with Compression Workflow + +**Compression with Encryption**: +``` +Input File → Compress → Encrypt → Output (.ext.jcze) +``` + +**Decompression with Decryption**: +``` +Input File (.jcze) → Detect Encryption → Decrypt → Decompress → Output +``` + +**Configuration Extension**: +```rust +pub struct CompressionConfig { + // ... existing fields ... + pub encryption: Option, +} + +pub struct DecompressionConfig { + pub move_to: Option, + pub force: bool, + pub decryption: Option, +} +``` + +### 11.9 Error Handling + +```rust +pub enum CryptoError { + InvalidPassword, + InvalidKey, + KeyDerivationFailed(String), + EncryptionFailed(String), + DecryptionFailed(String), + AuthenticationFailed, + InvalidContainer(String), + UnsupportedVersion(u8), + IoError(std::io::Error), + RsaError(String), + KeyFileNotFound(PathBuf), + KeyFileNotReadable(PathBuf), + InvalidPemFormat(String), + KeySizeTooSmall { actual: usize, minimum: usize }, +} +``` + +### 11.10 Dependencies + +- `ring` (v0.17): AES-256-GCM encryption, CSPRNG +- `rsa` (v0.9): RSA operations +- `argon2` (v0.5): Password hashing +- `pem` (v3.0): PEM file parsing +- `zeroize` (v1.7): Secure memory clearing +- `sha2` (v0.10): SHA-256 hashing +- `rand` (v0.8): Random number generation +- `rpassword` (v7.3): Secure password input + +### 11.11 Testing Strategy + +**Unit Tests**: +- Container serialization/deserialization +- Key derivation with known test vectors +- Encryption/decryption round-trips +- Error condition handling + +**Property-Based Tests** (using `proptest`): +- Password encryption round-trip for arbitrary data +- RSA encryption round-trip for arbitrary data +- Wrong password authentication failure +- Random value uniqueness (salts, nonces, keys) +- Metadata completeness + +**Integration Tests**: +- End-to-end encryption/decryption with all compression formats +- Batch file encryption/decryption +- CLI argument validation +- Error handling for various failure scenarios + +### 11.12 Security Considerations + +1. **Password Security**: + - Passwords never stored or logged + - Secure prompting without echo + - Immediate zeroization after use + +2. **Key Material**: + - Symmetric keys generated using CSPRNG + - Keys zeroized after use + - Private keys never written by application + +3. **Cryptographic Parameters**: + - Argon2id: 64MB memory, 3 iterations, 4 threads + - AES-256-GCM: 256-bit keys, 96-bit nonces + - RSA-OAEP: SHA-256, minimum 2048-bit keys + +4. **Implementation**: + - Use of well-audited cryptographic libraries + - No custom cryptographic primitives + - Authenticated encryption prevents tampering + +--- + +## 12. Appendices ### Appendix A: Rust Ecosystem Alignment @@ -2517,4 +2759,16 @@ Key differences: --- +## 13. Version History + +| Version | Date | Changes | +|---------|------|---------| +| 1.0 | 2025-11-30 | Initial release | +| 1.1 | 2025-11-30 | Added detailed design sections | +| 1.2 | 2025-12-01 | Added encryption module design | + +--- + **End of Software Design Document** +--- + diff --git a/docs/jcz_srs.md b/docs/jcz_srs.md index ec631a6..98e6240 100644 --- a/docs/jcz_srs.md +++ b/docs/jcz_srs.md @@ -1,8 +1,8 @@ # Software Requirements Specification (SRS) ## JCZ - Just Compress Zip Utility -**Version:** 1.1 -**Date:** 2025-11-30 +**Version:** 1.2 +**Date:** 2025-12-01 **Document Status:** Final --- @@ -32,6 +32,10 @@ JCZ is a command-line tool that simplifies file and directory compression/decomp - **TGZ**: TAR + GZIP compound format (.tar.gz) - **TBZ2**: TAR + BZIP2 compound format (.tar.bz2) - **TXZ**: TAR + XZ compound format (.tar.xz) +- **AES-GCM**: Advanced Encryption Standard in Galois/Counter Mode +- **RSA**: Rivest-Shamir-Adleman public-key cryptosystem +- **Argon2id**: Memory-hard password hashing function +- **JCZE**: JCZ Encrypted file extension (.jcze) --- @@ -43,11 +47,13 @@ JCZ is a standalone command-line utility that wraps system compression tools (gz ### 2.2 Product Features 1. **Multi-Format Compression**: Support for GZIP, BZIP2, XZ, ZIP, TAR, TGZ, TBZ2, TXZ 2. **Multi-Format Decompression**: Automatic format detection and sequential decompression -3. **Isolated Decompression**: Temporary directory isolation prevents file conflicts -4. **Force Overwrite**: Skip interactive prompts with --force/-f flag -5. **Auto-Create Directories**: Automatically create destination directories with -C flag -6. **Multi-File Archive Support**: Correctly handle TAR archives with multiple files -7. **Configurable Compression Levels**: User-selectable compression ratios (1-9) +3. **File Encryption**: Password-based and RSA public-key encryption for compressed files +4. **Secure Decryption**: Automatic encrypted file detection and decryption +5. **Isolated Decompression**: Temporary directory isolation prevents file conflicts +6. **Force Overwrite**: Skip interactive prompts with --force/-f flag +7. **Auto-Create Directories**: Automatically create destination directories with -C flag +8. **Multi-File Archive Support**: Correctly handle TAR archives with multiple files +9. **Configurable Compression Levels**: User-selectable compression ratios (1-9) 8. **Timestamp Appending**: Optional timestamp suffixes for output files 9. **File Collection**: Combine multiple files into single archive 10. **Output Relocation**: Move compressed/decompressed files to specified directories @@ -595,6 +601,125 @@ jcz [Options] [File|Dir]... - Support streaming writes where possible - Ensure atomic file creation (create complete file or fail) +#### 3.2.5 Encryption Requirements + +##### FR-ENCRYPT-001: Password-Based Encryption +**Description**: The system shall encrypt compressed files using password-based encryption. + +**Inputs**: +- Compressed file +- User password (prompted securely) + +**Processing**: +1. Prompt user for password without echo +2. Generate cryptographically secure random salt (32 bytes) +3. Derive encryption key using Argon2id (64MB memory, 3 iterations) +4. Generate random nonce (12 bytes) +5. Encrypt data using AES-256-GCM +6. Create encrypted container with metadata +7. Write to file with .jcze extension + +**Outputs**: +- Encrypted file (.jcze) +- Success/error status + +**Error Conditions**: +- Empty password +- Encryption failure +- File write error + +##### FR-ENCRYPT-002: RSA Public-Key Encryption +**Description**: The system shall encrypt compressed files using RSA public-key cryptography. + +**Inputs**: +- Compressed file +- RSA public key file (PEM format, minimum 2048 bits) + +**Processing**: +1. Validate and parse RSA public key +2. Generate random AES-256 symmetric key +3. Generate random nonce (12 bytes) +4. Encrypt data using AES-256-GCM with symmetric key +5. Encrypt symmetric key using RSA-OAEP-SHA256 +6. Create encrypted container with encrypted key and metadata +7. Write to file with .jcze extension + +**Outputs**: +- Encrypted file (.jcze) +- Success/error status + +**Error Conditions**: +- Invalid or missing key file +- Key size too small (< 2048 bits) +- Invalid PEM format +- Encryption failure + +##### FR-DECRYPT-001: Password-Based Decryption +**Description**: The system shall decrypt password-encrypted files. + +**Inputs**: +- Encrypted file (.jcze) +- User password (prompted securely) + +**Processing**: +1. Read and parse encrypted container +2. Detect password-based encryption from metadata +3. Prompt user for password +4. Derive decryption key using stored salt and Argon2 parameters +5. Decrypt data using AES-256-GCM +6. Verify authentication tag +7. Write decrypted data +8. Remove encrypted file + +**Outputs**: +- Decrypted compressed file +- Success/error status + +**Error Conditions**: +- Incorrect password (authentication failure) +- Corrupted encrypted file +- Invalid container format + +##### FR-DECRYPT-002: RSA Private-Key Decryption +**Description**: The system shall decrypt RSA-encrypted files using private keys. + +**Inputs**: +- Encrypted file (.jcze) +- RSA private key file (PEM format) + +**Processing**: +1. Read and parse encrypted container +2. Validate and parse RSA private key +3. Decrypt symmetric key using RSA-OAEP-SHA256 +4. Decrypt data using AES-256-GCM with recovered symmetric key +5. Verify authentication tag +6. Write decrypted data +7. Remove encrypted file + +**Outputs**: +- Decrypted compressed file +- Success/error status + +**Error Conditions**: +- Invalid or missing key file +- Wrong key (decryption failure) +- Corrupted encrypted file +- Authentication failure + +##### FR-ENCRYPT-003: Automatic Encrypted File Detection +**Description**: The system shall automatically detect and handle encrypted files during decompression. + +**Inputs**: +- File path (may be encrypted) + +**Processing**: +1. Check file extension for .jcze +2. If encrypted, decrypt before decompressing +3. If not encrypted, proceed with normal decompression + +**Outputs**: +- Appropriate handling based on encryption status + --- ### 3.3 Non-Functional Requirements diff --git a/src/cli/args.rs b/src/cli/args.rs index 1305dd1..c25f260 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -62,6 +62,22 @@ EXAMPLES: # Force overwrite without prompting jcz -d -f archive.tar.gz +ENCRYPTION: + # Encrypt with password + jcz -c gzip -e file.txt + + # Encrypt with RSA public key + jcz -c gzip --encrypt-key public.pem file.txt + + # Decrypt with password (will prompt) + jcz -d file.txt.gz.jcze + + # Decrypt with RSA private key + jcz -d --decrypt-key private.pem file.txt.gz.jcze + + # Decrypt and remove encrypted file + jcz -d --remove-encrypted file.txt.gz.jcze + ENVIRONMENT VARIABLES: JCDBG Control logging verbosity (error, warn, info, debug) @@ -110,6 +126,22 @@ pub struct CliArgs { /// Input files or directories #[arg(required = true)] pub inputs: Vec, + + /// Enable password-based encryption + #[arg(long = "encrypt-password", short = 'e')] + pub encrypt_password: bool, + + /// RSA public key file for encryption (encrypts the symmetric key) + #[arg(long = "encrypt-key")] + pub encrypt_key: Option, + + /// RSA private key file for decryption (decrypts the symmetric key) + #[arg(long = "decrypt-key")] + pub decrypt_key: Option, + + /// Remove encrypted file after successful decryption + #[arg(long = "remove-encrypted")] + pub remove_encrypted: bool, } impl CliArgs { @@ -131,6 +163,199 @@ impl CliArgs { return Err("Cannot specify both -a and -A".to_string()); } + // Check that password and RSA encryption are not both specified + if self.encrypt_password && self.encrypt_key.is_some() { + return Err("Cannot specify both --encrypt-password and --encrypt-key".to_string()); + } + + // Check that encryption options are only used in compression mode + if self.decompress { + if self.encrypt_password { + return Err("--encrypt-password can only be used in compression mode".to_string()); + } + if self.encrypt_key.is_some() { + return Err("--encrypt-key can only be used in compression mode".to_string()); + } + } + + // Check that decryption key is only used in decompression mode + if !self.decompress && self.decrypt_key.is_some() { + return Err("--decrypt-key can only be used in decompression mode".to_string()); + } + + // Check that remove-encrypted is only used in decompression mode + if !self.decompress && self.remove_encrypted { + return Err("--remove-encrypted can only be used in decompression mode".to_string()); + } + Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_mutual_exclusivity_password_and_rsa() { + let args = CliArgs { + decompress: false, + force: false, + command: "gzip".to_string(), + level: 6, + move_to: None, + collect: None, + collect_flat: None, + timestamp: 0, + inputs: vec![PathBuf::from("file.txt")], + encrypt_password: true, + encrypt_key: Some(PathBuf::from("key.pem")), + decrypt_key: None, + remove_encrypted: false, + }; + + let result = args.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("Cannot specify both --encrypt-password and --encrypt-key")); + } + + #[test] + fn test_validate_encrypt_password_only_in_compression() { + let args = CliArgs { + decompress: true, + force: false, + command: "gzip".to_string(), + level: 6, + move_to: None, + collect: None, + collect_flat: None, + timestamp: 0, + inputs: vec![PathBuf::from("file.txt.gz")], + encrypt_password: true, + encrypt_key: None, + decrypt_key: None, + remove_encrypted: false, + }; + + let result = args.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("--encrypt-password can only be used in compression mode")); + } + + #[test] + fn test_validate_encrypt_key_only_in_compression() { + let args = CliArgs { + decompress: true, + force: false, + command: "gzip".to_string(), + level: 6, + move_to: None, + collect: None, + collect_flat: None, + timestamp: 0, + inputs: vec![PathBuf::from("file.txt.gz")], + encrypt_password: false, + encrypt_key: Some(PathBuf::from("key.pem")), + decrypt_key: None, + remove_encrypted: false, + }; + + let result = args.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("--encrypt-key can only be used in compression mode")); + } + + #[test] + fn test_validate_decrypt_key_only_in_decompression() { + let args = CliArgs { + decompress: false, + force: false, + command: "gzip".to_string(), + level: 6, + move_to: None, + collect: None, + collect_flat: None, + timestamp: 0, + inputs: vec![PathBuf::from("file.txt")], + encrypt_password: false, + encrypt_key: None, + decrypt_key: Some(PathBuf::from("key.pem")), + remove_encrypted: false, + }; + + let result = args.validate(); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .contains("--decrypt-key can only be used in decompression mode")); + } + + #[test] + fn test_validate_valid_password_encryption() { + let args = CliArgs { + decompress: false, + force: false, + command: "gzip".to_string(), + level: 6, + move_to: None, + collect: None, + collect_flat: None, + timestamp: 0, + inputs: vec![PathBuf::from("file.txt")], + encrypt_password: true, + encrypt_key: None, + decrypt_key: None, + remove_encrypted: false, + }; + + assert!(args.validate().is_ok()); + } + + #[test] + fn test_validate_valid_rsa_encryption() { + let args = CliArgs { + decompress: false, + force: false, + command: "gzip".to_string(), + level: 6, + move_to: None, + collect: None, + collect_flat: None, + timestamp: 0, + inputs: vec![PathBuf::from("file.txt")], + encrypt_password: false, + encrypt_key: Some(PathBuf::from("public.pem")), + decrypt_key: None, + remove_encrypted: false, + }; + + assert!(args.validate().is_ok()); + } + + #[test] + fn test_validate_valid_rsa_decryption() { + let args = CliArgs { + decompress: true, + force: false, + command: "gzip".to_string(), + level: 6, + move_to: None, + collect: None, + collect_flat: None, + timestamp: 0, + inputs: vec![PathBuf::from("file.txt.gz.jcze")], + encrypt_password: false, + encrypt_key: None, + decrypt_key: Some(PathBuf::from("private.pem")), + remove_encrypted: false, + }; + + assert!(args.validate().is_ok()); + } +} diff --git a/src/cli/commands.rs b/src/cli/commands.rs index e0c690d..9f1ece5 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -1,7 +1,10 @@ use std::path::PathBuf; use crate::cli::args::CliArgs; -use crate::core::config::{CollectionConfig, CollectionMode, CompressionConfig, TimestampOption}; +use crate::core::config::{ + CollectionConfig, CollectionMode, CompressionConfig, DecryptionMethod, EncryptionMethod, + TimestampOption, +}; use crate::core::error::{JcError, JcResult}; use crate::core::types::{CompoundFormat, CompressionFormat}; use crate::operations::{collect_and_compress, compound, compress, decompress}; @@ -28,13 +31,36 @@ pub fn execute(args: CliArgs) -> JcResult<()> { config }; + // Add encryption configuration if specified + let config = if args.encrypt_password { + config.with_encryption(Some(EncryptionMethod::Password)) + } else if let Some(ref public_key_path) = args.encrypt_key { + config.with_encryption(Some(EncryptionMethod::Rsa { + public_key_path: public_key_path.clone(), + })) + } else { + config + }; + // Validate input files let inputs = validate_input_files(args.inputs)?; let input_paths: Vec = inputs.iter().map(|f| f.real_path.clone()).collect(); if args.decompress { // Decompression mode - handle_decompress(input_paths, config) + let decryption_method = if let Some(ref private_key_path) = args.decrypt_key { + Some(DecryptionMethod::Rsa { + private_key_path: private_key_path.clone(), + }) + } else { + None + }; + handle_decompress( + input_paths, + config, + decryption_method, + args.remove_encrypted, + ) } else if args.collect.is_some() || args.collect_flat.is_some() { // Collection mode let mode = if args.collect.is_some() { @@ -52,8 +78,13 @@ pub fn execute(args: CliArgs) -> JcResult<()> { } } -fn handle_decompress(inputs: Vec, config: CompressionConfig) -> JcResult<()> { - let results = decompress::decompress_files(inputs, config); +fn handle_decompress( + inputs: Vec, + config: CompressionConfig, + decryption_method: Option, + remove_encrypted: bool, +) -> JcResult<()> { + let results = decompress::decompress_files(inputs, config, decryption_method, remove_encrypted); // Check for errors let mut had_errors = false; diff --git a/src/core/config.rs b/src/core/config.rs index 1c19566..1b29d34 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -21,6 +21,15 @@ impl TimestampOption { } } +/// Encryption method for compression +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EncryptionMethod { + /// Password-based encryption + Password, + /// RSA encryption with public key file path + Rsa { public_key_path: PathBuf }, +} + /// Configuration for compression/decompression operations #[derive(Debug, Clone)] pub struct CompressionConfig { @@ -39,6 +48,9 @@ pub struct CompressionConfig { /// Force overwrite without prompting pub force: bool, + + /// Encryption method (if any) + pub encryption: Option, } impl Default for CompressionConfig { @@ -49,6 +61,7 @@ impl Default for CompressionConfig { move_to: None, show_output_size: false, force: false, + encryption: None, } } } @@ -77,6 +90,11 @@ impl CompressionConfig { self.force = force; self } + + pub fn with_encryption(mut self, encryption: Option) -> Self { + self.encryption = encryption; + self + } } /// Collection operation mode @@ -89,6 +107,74 @@ pub enum CollectionMode { Flat, } +/// Decryption method for decompression +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(dead_code)] +pub enum DecryptionMethod { + /// Password-based decryption (will prompt for password) + Password, + /// RSA decryption with private key file path + Rsa { private_key_path: PathBuf }, +} + +/// Configuration for decompression operations +#[derive(Debug, Clone)] +pub struct DecompressionConfig { + /// Destination directory for output files + pub move_to: Option, + + /// Force overwrite without prompting + pub force: bool, + + /// Decryption method (if any) + pub decryption: Option, + + /// Remove encrypted file after successful decryption + pub remove_encrypted: bool, +} + +impl Default for DecompressionConfig { + fn default() -> Self { + Self { + move_to: None, + force: false, + decryption: None, + remove_encrypted: false, + } + } +} + +impl DecompressionConfig { + #[allow(dead_code)] + pub fn new() -> Self { + Self::default() + } + + #[allow(dead_code)] + pub fn with_move_to(mut self, path: PathBuf) -> Self { + self.move_to = Some(path); + self + } + + #[allow(dead_code)] + pub fn with_force(mut self, force: bool) -> Self { + self.force = force; + self + } + + #[allow(dead_code)] + pub fn with_decryption(mut self, decryption: Option) -> Self { + self.decryption = decryption; + self + } + + #[allow(dead_code)] + pub fn with_remove_encrypted(mut self, remove_encrypted: bool) -> Self { + self.remove_encrypted = remove_encrypted; + self + } +} + /// Configuration for collection operations (multi-file archives) #[derive(Debug, Clone)] pub struct CollectionConfig { diff --git a/src/crypto/container.rs b/src/crypto/container.rs new file mode 100644 index 0000000..39f5c42 --- /dev/null +++ b/src/crypto/container.rs @@ -0,0 +1,394 @@ +//! Encrypted container format implementation + +use super::{CryptoError, CryptoResult, EncryptionMetadata, EncryptionType}; +use std::io::{Read, Write}; +use std::path::Path; + +/// Magic bytes for JCZ encrypted files: "JCZE" +const MAGIC_BYTES: [u8; 4] = [0x4A, 0x43, 0x5A, 0x45]; + +/// Current container format version +const CONTAINER_VERSION: u8 = 1; + +/// Encrypted container structure +#[derive(Debug, Clone)] +pub struct EncryptedContainer { + /// Container format version + pub version: u8, + /// Encryption type + pub encryption_type: EncryptionType, + /// Encryption metadata + pub metadata: EncryptionMetadata, + /// Encrypted data + pub encrypted_data: Vec, +} + +impl EncryptedContainer { + /// Create a new encrypted container + pub fn new( + encryption_type: EncryptionType, + metadata: EncryptionMetadata, + encrypted_data: Vec, + ) -> Self { + Self { + version: CONTAINER_VERSION, + encryption_type, + metadata, + encrypted_data, + } + } + + /// Write container to file + pub fn write_to_file(&self, path: &Path) -> CryptoResult<()> { + let mut file = std::fs::File::create(path)?; + let bytes = self.to_bytes()?; + file.write_all(&bytes)?; + Ok(()) + } + + /// Read container from file + pub fn read_from_file(path: &Path) -> CryptoResult { + let mut file = std::fs::File::open(path)?; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes)?; + Self::from_bytes(&bytes) + } + + /// Serialize container to bytes + pub fn to_bytes(&self) -> CryptoResult> { + let mut bytes = Vec::new(); + + // Magic bytes + bytes.extend_from_slice(&MAGIC_BYTES); + + // Version + bytes.push(self.version); + + // Encryption type + bytes.push(self.encryption_type.to_u8()); + + // Serialize metadata + let metadata_bytes = self.serialize_metadata()?; + let metadata_len = metadata_bytes.len() as u32; + bytes.extend_from_slice(&metadata_len.to_le_bytes()); + bytes.extend_from_slice(&metadata_bytes); + + // Encrypted data + bytes.extend_from_slice(&self.encrypted_data); + + Ok(bytes) + } + + /// Deserialize container from bytes + pub fn from_bytes(bytes: &[u8]) -> CryptoResult { + if bytes.len() < 10 { + return Err(CryptoError::InvalidContainer( + "Container too small".to_string(), + )); + } + + let mut pos = 0; + + // Check magic bytes + if &bytes[pos..pos + 4] != &MAGIC_BYTES { + return Err(CryptoError::InvalidContainer( + "Invalid magic bytes".to_string(), + )); + } + pos += 4; + + // Read version + let version = bytes[pos]; + pos += 1; + + if version != CONTAINER_VERSION { + return Err(CryptoError::UnsupportedVersion(version)); + } + + // Read encryption type + let encryption_type = EncryptionType::from_u8(bytes[pos]) + .ok_or_else(|| CryptoError::InvalidContainer("Invalid encryption type".to_string()))?; + pos += 1; + + // Read metadata length + if bytes.len() < pos + 4 { + return Err(CryptoError::InvalidContainer( + "Missing metadata length".to_string(), + )); + } + let metadata_len = + u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]) + as usize; + pos += 4; + + // Read metadata + if bytes.len() < pos + metadata_len { + return Err(CryptoError::InvalidContainer( + "Truncated metadata".to_string(), + )); + } + let metadata_bytes = &bytes[pos..pos + metadata_len]; + let metadata = Self::deserialize_metadata(encryption_type, metadata_bytes)?; + pos += metadata_len; + + // Read encrypted data + let encrypted_data = bytes[pos..].to_vec(); + + Ok(Self { + version, + encryption_type, + metadata, + encrypted_data, + }) + } + + /// Serialize metadata to bytes + fn serialize_metadata(&self) -> CryptoResult> { + let mut bytes = Vec::new(); + + match &self.metadata { + EncryptionMetadata::Password { + salt, + nonce, + argon2_params, + } => { + bytes.extend_from_slice(salt); + bytes.extend_from_slice(nonce); + bytes.extend_from_slice(&argon2_params.memory_cost.to_le_bytes()); + bytes.extend_from_slice(&argon2_params.time_cost.to_le_bytes()); + bytes.extend_from_slice(&argon2_params.parallelism.to_le_bytes()); + } + EncryptionMetadata::Rsa { + encrypted_key, + nonce, + } => { + let key_len = encrypted_key.len() as u32; + bytes.extend_from_slice(&key_len.to_le_bytes()); + bytes.extend_from_slice(encrypted_key); + bytes.extend_from_slice(nonce); + } + } + + Ok(bytes) + } + + /// Deserialize metadata from bytes + fn deserialize_metadata( + encryption_type: EncryptionType, + bytes: &[u8], + ) -> CryptoResult { + match encryption_type { + EncryptionType::Password => { + if bytes.len() < 32 + 12 + 12 { + return Err(CryptoError::InvalidContainer( + "Invalid password metadata size".to_string(), + )); + } + + let mut salt = [0u8; 32]; + salt.copy_from_slice(&bytes[0..32]); + + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&bytes[32..44]); + + let memory_cost = u32::from_le_bytes([bytes[44], bytes[45], bytes[46], bytes[47]]); + let time_cost = u32::from_le_bytes([bytes[48], bytes[49], bytes[50], bytes[51]]); + let parallelism = u32::from_le_bytes([bytes[52], bytes[53], bytes[54], bytes[55]]); + + Ok(EncryptionMetadata::Password { + salt, + nonce, + argon2_params: super::Argon2Params { + memory_cost, + time_cost, + parallelism, + }, + }) + } + EncryptionType::Rsa => { + if bytes.len() < 4 { + return Err(CryptoError::InvalidContainer( + "Invalid RSA metadata size".to_string(), + )); + } + + let key_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize; + + if bytes.len() < 4 + key_len + 12 { + return Err(CryptoError::InvalidContainer( + "Truncated RSA metadata".to_string(), + )); + } + + let encrypted_key = bytes[4..4 + key_len].to_vec(); + + let mut nonce = [0u8; 12]; + nonce.copy_from_slice(&bytes[4 + key_len..4 + key_len + 12]); + + Ok(EncryptionMetadata::Rsa { + encrypted_key, + nonce, + }) + } + } + } + + /// Get the encryption type + #[allow(dead_code)] + pub fn get_encryption_type(&self) -> EncryptionType { + self.encryption_type + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + // Feature: file-encryption, Property 3: Encryption metadata completeness + // For any encrypted file (password or RSA), the stored metadata should contain + // all parameters necessary for decryption and should not contain the actual + // encryption key or password. + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test] + fn prop_password_container_round_trip( + salt in prop::array::uniform32(any::()), + nonce in prop::array::uniform12(any::()), + memory_cost in 1u32..100000u32, + time_cost in 1u32..10u32, + parallelism in 1u32..16u32, + encrypted_data in prop::collection::vec(any::(), 0..1000), + ) { + let metadata = EncryptionMetadata::Password { + salt, + nonce, + argon2_params: super::super::Argon2Params { + memory_cost, + time_cost, + parallelism, + }, + }; + + let container = EncryptedContainer::new( + EncryptionType::Password, + metadata.clone(), + encrypted_data.clone(), + ); + + // Serialize and deserialize + let bytes = container.to_bytes().unwrap(); + let recovered = EncryptedContainer::from_bytes(&bytes).unwrap(); + + // Verify all metadata is preserved + assert_eq!(recovered.encryption_type, EncryptionType::Password); + assert_eq!(recovered.encrypted_data, encrypted_data); + + if let EncryptionMetadata::Password { + salt: recovered_salt, + nonce: recovered_nonce, + argon2_params: recovered_params, + } = recovered.metadata + { + assert_eq!(recovered_salt, salt); + assert_eq!(recovered_nonce, nonce); + assert_eq!(recovered_params.memory_cost, memory_cost); + assert_eq!(recovered_params.time_cost, time_cost); + assert_eq!(recovered_params.parallelism, parallelism); + } else { + panic!("Expected Password metadata"); + } + + // Verify no sensitive data in metadata (salt and nonce are public, params are public) + // The actual password should never be stored + if let EncryptionMetadata::Password { salt: s, nonce: n, argon2_params: p } = metadata { + // These are all non-sensitive parameters + assert_eq!(s.len(), 32); + assert_eq!(n.len(), 12); + assert!(p.memory_cost > 0); + assert!(p.time_cost > 0); + assert!(p.parallelism > 0); + } + } + + #[test] + fn prop_rsa_container_round_trip( + encrypted_key in prop::collection::vec(any::(), 1..512), + nonce in prop::array::uniform12(any::()), + encrypted_data in prop::collection::vec(any::(), 0..1000), + ) { + let metadata = EncryptionMetadata::Rsa { + encrypted_key: encrypted_key.clone(), + nonce, + }; + + let container = EncryptedContainer::new( + EncryptionType::Rsa, + metadata.clone(), + encrypted_data.clone(), + ); + + // Serialize and deserialize + let bytes = container.to_bytes().unwrap(); + let recovered = EncryptedContainer::from_bytes(&bytes).unwrap(); + + // Verify all metadata is preserved + assert_eq!(recovered.encryption_type, EncryptionType::Rsa); + assert_eq!(recovered.encrypted_data, encrypted_data); + + if let EncryptionMetadata::Rsa { + encrypted_key: recovered_key, + nonce: recovered_nonce, + } = recovered.metadata + { + assert_eq!(recovered_key, encrypted_key); + assert_eq!(recovered_nonce, nonce); + } else { + panic!("Expected RSA metadata"); + } + + // Verify the encrypted symmetric key is stored (not the plaintext key) + // and nonce is public + if let EncryptionMetadata::Rsa { encrypted_key: ek, nonce: n } = metadata { + assert!(!ek.is_empty()); + assert_eq!(n.len(), 12); + } + } + } + + #[test] + fn test_invalid_magic_bytes() { + let bad_bytes = vec![0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00]; + let result = EncryptedContainer::from_bytes(&bad_bytes); + assert!(result.is_err()); + if let Err(CryptoError::InvalidContainer(msg)) = result { + assert!(msg.contains("magic bytes")); + } else { + panic!("Expected InvalidContainer error"); + } + } + + #[test] + fn test_unsupported_version() { + let mut bytes = vec![0x4A, 0x43, 0x5A, 0x45]; // Magic bytes + bytes.push(99); // Invalid version + bytes.extend_from_slice(&[0x01, 0x00, 0x00, 0x00, 0x00]); + + let result = EncryptedContainer::from_bytes(&bytes); + assert!(result.is_err()); + if let Err(CryptoError::UnsupportedVersion(v)) = result { + assert_eq!(v, 99); + } else { + panic!("Expected UnsupportedVersion error"); + } + } + + #[test] + fn test_truncated_container() { + let bytes = vec![0x4A, 0x43, 0x5A, 0x45, 0x01]; // Too short + let result = EncryptedContainer::from_bytes(&bytes); + assert!(result.is_err()); + } +} diff --git a/src/crypto/keys.rs b/src/crypto/keys.rs new file mode 100644 index 0000000..2875fe7 --- /dev/null +++ b/src/crypto/keys.rs @@ -0,0 +1,172 @@ +//! Key management utilities + +use super::{CryptoError, CryptoResult}; +use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey}; +use rsa::traits::PublicKeyParts; +use rsa::{RsaPrivateKey, RsaPublicKey}; +use std::fs; +use std::path::Path; + +/// Validate that a key file exists and is readable +pub fn validate_key_file(path: &Path) -> CryptoResult<()> { + if !path.exists() { + return Err(CryptoError::KeyFileNotFound(path.to_path_buf())); + } + + if !path.is_file() { + return Err(CryptoError::KeyFileNotReadable(path.to_path_buf())); + } + + // Try to read the file to ensure it's readable + fs::metadata(path).map_err(|_| CryptoError::KeyFileNotReadable(path.to_path_buf()))?; + + Ok(()) +} + +/// Read and parse RSA private key from PEM file +pub fn read_private_key_pem(path: &Path) -> CryptoResult { + // Validate file exists and is readable + validate_key_file(path)?; + + // Read file contents + let pem_data = fs::read_to_string(path) + .map_err(|e| CryptoError::InvalidPemFormat(format!("Failed to read file: {}", e)))?; + + // Decode RSA private key from PEM + let private_key = RsaPrivateKey::from_pkcs8_pem(&pem_data).map_err(|e| { + CryptoError::InvalidPemFormat(format!("Failed to decode private key: {}", e)) + })?; + + // Validate key size + let key_size = private_key.size() * 8; // size() returns bytes, convert to bits + validate_key_size(key_size)?; + + Ok(private_key) +} + +/// Read and parse RSA public key from PEM file +pub fn read_public_key_pem(path: &Path) -> CryptoResult { + // Validate file exists and is readable + validate_key_file(path)?; + + // Read file contents + let pem_data = fs::read_to_string(path) + .map_err(|e| CryptoError::InvalidPemFormat(format!("Failed to read file: {}", e)))?; + + // Decode RSA public key from PEM + let public_key = RsaPublicKey::from_public_key_pem(&pem_data).map_err(|e| { + CryptoError::InvalidPemFormat(format!("Failed to decode public key: {}", e)) + })?; + + // Validate key size + let key_size = public_key.size() * 8; // size() returns bytes, convert to bits + validate_key_size(key_size)?; + + Ok(public_key) +} + +/// Validate RSA key size (minimum 2048 bits) +pub fn validate_key_size(key_bits: usize) -> CryptoResult<()> { + const MIN_KEY_SIZE: usize = 2048; + if key_bits < MIN_KEY_SIZE { + return Err(CryptoError::KeySizeTooSmall { + actual: key_bits, + minimum: MIN_KEY_SIZE, + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}; + use rsa::RsaPrivateKey; + use std::io::Write; + use tempfile::NamedTempFile; + + fn create_test_key_pair() -> (RsaPrivateKey, RsaPublicKey) { + use rand::rngs::OsRng; + let mut rng = OsRng; + let private_key = RsaPrivateKey::new(&mut rng, 2048).unwrap(); + let public_key = RsaPublicKey::from(&private_key); + (private_key, public_key) + } + + #[test] + fn test_validate_key_file_not_found() { + let result = validate_key_file(Path::new("/nonexistent/file.pem")); + assert!(result.is_err()); + if let Err(CryptoError::KeyFileNotFound(_)) = result { + // Expected + } else { + panic!("Expected KeyFileNotFound error"); + } + } + + #[test] + fn test_read_private_key_pem() { + let (private_key, _) = create_test_key_pair(); + + // Write to temporary file + let mut temp_file = NamedTempFile::new().unwrap(); + let pem_data = private_key + .to_pkcs8_pem(LineEnding::LF) + .unwrap() + .to_string(); + temp_file.write_all(pem_data.as_bytes()).unwrap(); + temp_file.flush().unwrap(); + + // Read back + let loaded_key = read_private_key_pem(temp_file.path()).unwrap(); + + // Verify key size + assert_eq!(loaded_key.size(), private_key.size()); + } + + #[test] + fn test_read_public_key_pem() { + let (_, public_key) = create_test_key_pair(); + + // Write to temporary file + let mut temp_file = NamedTempFile::new().unwrap(); + let pem_data = public_key.to_public_key_pem(LineEnding::LF).unwrap(); + temp_file.write_all(pem_data.as_bytes()).unwrap(); + temp_file.flush().unwrap(); + + // Read back + let loaded_key = read_public_key_pem(temp_file.path()).unwrap(); + + // Verify key size + assert_eq!(loaded_key.size(), public_key.size()); + } + + #[test] + fn test_validate_key_size() { + assert!(validate_key_size(2048).is_ok()); + assert!(validate_key_size(4096).is_ok()); + assert!(validate_key_size(1024).is_err()); + + if let Err(CryptoError::KeySizeTooSmall { actual, minimum }) = validate_key_size(1024) { + assert_eq!(actual, 1024); + assert_eq!(minimum, 2048); + } else { + panic!("Expected KeySizeTooSmall error"); + } + } + + #[test] + fn test_read_invalid_pem() { + let mut temp_file = NamedTempFile::new().unwrap(); + temp_file.write_all(b"not a valid PEM file").unwrap(); + temp_file.flush().unwrap(); + + let result = read_private_key_pem(temp_file.path()); + assert!(result.is_err()); + if let Err(CryptoError::InvalidPemFormat(_)) = result { + // Expected + } else { + panic!("Expected InvalidPemFormat error"); + } + } +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs new file mode 100644 index 0000000..d2cdfe3 --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,167 @@ +//! Cryptography module for file encryption and decryption +//! +//! This module provides encryption capabilities for compressed files using: +//! - Password-based encryption with AES-256-GCM and Argon2id key derivation +//! - RSA public-key encryption with OAEP padding + +pub mod container; +pub mod keys; +pub mod password; +pub mod rsa; + +use std::path::PathBuf; + +// Re-export commonly used types +pub use container::EncryptedContainer; +pub use password::PasswordEncryption; +pub use rsa::RsaEncryption; + +/// Encryption type identifier +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EncryptionType { + /// Password-based encryption (0x01) + Password = 0x01, + /// RSA encryption (0x02) + Rsa = 0x02, +} + +impl EncryptionType { + /// Convert from byte value + pub fn from_u8(value: u8) -> Option { + match value { + 0x01 => Some(EncryptionType::Password), + 0x02 => Some(EncryptionType::Rsa), + _ => None, + } + } + + /// Convert to byte value + pub fn to_u8(self) -> u8 { + self as u8 + } +} + +/// Argon2 parameters for key derivation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Argon2Params { + /// Memory cost in KB (default: 65536 = 64 MB) + pub memory_cost: u32, + /// Time cost (iterations, default: 3) + pub time_cost: u32, + /// Parallelism (threads, default: 4) + pub parallelism: u32, +} + +impl Default for Argon2Params { + fn default() -> Self { + Self { + memory_cost: 65536, // 64 MB + time_cost: 3, + parallelism: 4, + } + } +} + +/// Encryption metadata +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EncryptionMetadata { + /// Password-based encryption metadata + Password { + salt: [u8; 32], + nonce: [u8; 12], + argon2_params: Argon2Params, + }, + /// RSA encryption metadata + Rsa { + encrypted_key: Vec, + nonce: [u8; 12], + }, +} + +/// Cryptography error types +#[derive(Debug)] +#[allow(dead_code)] +pub enum CryptoError { + /// Invalid or empty password + InvalidPassword, + /// Invalid key + InvalidKey, + /// Key derivation failed + KeyDerivationFailed(String), + /// Encryption operation failed + EncryptionFailed(String), + /// Decryption operation failed + DecryptionFailed(String), + /// Authentication failed (wrong password/key) + AuthenticationFailed, + /// Invalid container format + InvalidContainer(String), + /// Unsupported container version + UnsupportedVersion(u8), + /// I/O error + IoError(std::io::Error), + /// RSA error + RsaError(String), + /// Key file not found + KeyFileNotFound(PathBuf), + /// Key file not readable + KeyFileNotReadable(PathBuf), + /// Invalid PEM format + InvalidPemFormat(String), + /// Key size too small + KeySizeTooSmall { actual: usize, minimum: usize }, +} + +impl std::fmt::Display for CryptoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CryptoError::InvalidPassword => write!(f, "Invalid or empty password"), + CryptoError::InvalidKey => write!(f, "Invalid encryption key"), + CryptoError::KeyDerivationFailed(msg) => write!(f, "Key derivation failed: {}", msg), + CryptoError::EncryptionFailed(msg) => write!(f, "Encryption failed: {}", msg), + CryptoError::DecryptionFailed(msg) => write!(f, "Decryption failed: {}", msg), + CryptoError::AuthenticationFailed => { + write!( + f, + "Authentication failed: incorrect password or corrupted data" + ) + } + CryptoError::InvalidContainer(msg) => write!(f, "Invalid container format: {}", msg), + CryptoError::UnsupportedVersion(v) => { + write!(f, "Unsupported container version: {}", v) + } + CryptoError::IoError(err) => write!(f, "I/O error: {}", err), + CryptoError::RsaError(msg) => write!(f, "RSA error: {}", msg), + CryptoError::KeyFileNotFound(path) => { + write!(f, "Key file not found: {}", path.display()) + } + CryptoError::KeyFileNotReadable(path) => { + write!(f, "Key file not readable: {}", path.display()) + } + CryptoError::InvalidPemFormat(msg) => write!(f, "Invalid PEM format: {}", msg), + CryptoError::KeySizeTooSmall { actual, minimum } => write!( + f, + "Key size too small: {} bits (minimum: {} bits)", + actual, minimum + ), + } + } +} + +impl std::error::Error for CryptoError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + CryptoError::IoError(err) => Some(err), + _ => None, + } + } +} + +impl From for CryptoError { + fn from(err: std::io::Error) -> Self { + CryptoError::IoError(err) + } +} + +/// Result type for crypto operations +pub type CryptoResult = Result; diff --git a/src/crypto/password.rs b/src/crypto/password.rs new file mode 100644 index 0000000..06f372f --- /dev/null +++ b/src/crypto/password.rs @@ -0,0 +1,455 @@ +//! Password-based encryption implementation + +use super::{Argon2Params, CryptoError, CryptoResult}; +use argon2::{Argon2, Version}; +use ring::rand::{SecureRandom, SystemRandom}; + +/// Password-based encryption operations +pub struct PasswordEncryption; + +impl PasswordEncryption { + /// Validate password (reject empty passwords) + pub fn validate_password(password: &str) -> CryptoResult<()> { + if password.is_empty() { + return Err(CryptoError::InvalidPassword); + } + Ok(()) + } + + /// Derive encryption key from password using Argon2id + pub fn derive_key( + password: &str, + salt: &[u8; 32], + params: &Argon2Params, + ) -> CryptoResult<[u8; 32]> { + // Validate password + Self::validate_password(password)?; + + // Build Argon2 parameters + let argon2_params = argon2::Params::new( + params.memory_cost, + params.time_cost, + params.parallelism, + Some(32), + ) + .map_err(|e| CryptoError::KeyDerivationFailed(format!("Invalid params: {}", e)))?; + + // Create Argon2id instance + let argon2 = Argon2::new(argon2::Algorithm::Argon2id, Version::V0x13, argon2_params); + + // Derive key directly into output buffer + let mut key = [0u8; 32]; + argon2 + .hash_password_into(password.as_bytes(), salt, &mut key) + .map_err(|e| { + CryptoError::KeyDerivationFailed(format!("Key derivation failed: {}", e)) + })?; + + Ok(key) + } + + /// Generate random salt using cryptographically secure RNG + pub fn generate_salt() -> CryptoResult<[u8; 32]> { + let rng = SystemRandom::new(); + let mut salt = [0u8; 32]; + rng.fill(&mut salt) + .map_err(|_| CryptoError::KeyDerivationFailed("Failed to generate salt".to_string()))?; + Ok(salt) + } + + /// Generate random nonce using cryptographically secure RNG + pub fn generate_nonce() -> CryptoResult<[u8; 12]> { + let rng = SystemRandom::new(); + let mut nonce = [0u8; 12]; + rng.fill(&mut nonce) + .map_err(|_| CryptoError::EncryptionFailed("Failed to generate nonce".to_string()))?; + Ok(nonce) + } + + /// Encrypt data with AES-256-GCM + pub fn encrypt(data: &[u8], key: &[u8; 32], nonce: &[u8; 12]) -> CryptoResult> { + use ring::aead::{ + Aad, BoundKey, Nonce, NonceSequence, SealingKey, UnboundKey, AES_256_GCM, + }; + use ring::error::Unspecified; + + // Create a nonce sequence that returns our nonce once + struct SingleNonce([u8; 12]); + + impl NonceSequence for SingleNonce { + fn advance(&mut self) -> Result { + Nonce::try_assume_unique_for_key(&self.0) + } + } + + // Create sealing key + let unbound_key = UnboundKey::new(&AES_256_GCM, key) + .map_err(|_| CryptoError::EncryptionFailed("Failed to create key".to_string()))?; + let nonce_sequence = SingleNonce(*nonce); + let mut sealing_key = SealingKey::new(unbound_key, nonce_sequence); + + // Prepare data for encryption (ring modifies in place) + let mut in_out = data.to_vec(); + + // Seal (encrypt and authenticate) + sealing_key + .seal_in_place_append_tag(Aad::empty(), &mut in_out) + .map_err(|_| CryptoError::EncryptionFailed("Encryption failed".to_string()))?; + + Ok(in_out) + } + + /// Decrypt data with AES-256-GCM + pub fn decrypt( + encrypted_data: &[u8], + key: &[u8; 32], + nonce: &[u8; 12], + ) -> CryptoResult> { + use ring::aead::{ + Aad, BoundKey, Nonce, NonceSequence, OpeningKey, UnboundKey, AES_256_GCM, + }; + use ring::error::Unspecified; + + // Create a nonce sequence that returns our nonce once + struct SingleNonce([u8; 12]); + + impl NonceSequence for SingleNonce { + fn advance(&mut self) -> Result { + Nonce::try_assume_unique_for_key(&self.0) + } + } + + // Create opening key + let unbound_key = UnboundKey::new(&AES_256_GCM, key) + .map_err(|_| CryptoError::DecryptionFailed("Failed to create key".to_string()))?; + let nonce_sequence = SingleNonce(*nonce); + let mut opening_key = OpeningKey::new(unbound_key, nonce_sequence); + + // Prepare data for decryption (ring modifies in place) + let mut in_out = encrypted_data.to_vec(); + + // Open (decrypt and verify authentication) + let decrypted = opening_key + .open_in_place(Aad::empty(), &mut in_out) + .map_err(|_| CryptoError::AuthenticationFailed)?; + + Ok(decrypted.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_password() { + assert!(PasswordEncryption::validate_password("valid_password").is_ok()); + assert!(PasswordEncryption::validate_password("").is_err()); + } + + #[test] + fn test_generate_salt() { + let salt1 = PasswordEncryption::generate_salt().unwrap(); + let salt2 = PasswordEncryption::generate_salt().unwrap(); + + // Salts should be different + assert_ne!(salt1, salt2); + assert_eq!(salt1.len(), 32); + assert_eq!(salt2.len(), 32); + } + + #[test] + fn test_generate_nonce() { + let nonce1 = PasswordEncryption::generate_nonce().unwrap(); + let nonce2 = PasswordEncryption::generate_nonce().unwrap(); + + // Nonces should be different + assert_ne!(nonce1, nonce2); + assert_eq!(nonce1.len(), 12); + assert_eq!(nonce2.len(), 12); + } + + #[test] + fn test_encrypt() { + let data = b"Hello, World!"; + let key = [42u8; 32]; + let nonce = [1u8; 12]; + + let encrypted = PasswordEncryption::encrypt(data, &key, &nonce).unwrap(); + + // Encrypted data should be longer (includes auth tag) + assert!(encrypted.len() > data.len()); + // Encrypted data should be different from plaintext + assert_ne!(&encrypted[..data.len()], data); + } + + #[test] + fn test_decrypt() { + let data = b"Hello, World!"; + let key = [42u8; 32]; + let nonce = [1u8; 12]; + + // First encrypt + let encrypted = PasswordEncryption::encrypt(data, &key, &nonce).unwrap(); + + // Then decrypt + let decrypted = PasswordEncryption::decrypt(&encrypted, &key, &nonce).unwrap(); + + // Should match original + assert_eq!(decrypted, data); + } + + #[test] + fn test_decrypt_wrong_key() { + let data = b"Hello, World!"; + let key1 = [42u8; 32]; + let key2 = [43u8; 32]; + let nonce = [1u8; 12]; + + let encrypted = PasswordEncryption::encrypt(data, &key1, &nonce).unwrap(); + + // Try to decrypt with wrong key + let result = PasswordEncryption::decrypt(&encrypted, &key2, &nonce); + assert!(result.is_err()); + if let Err(CryptoError::AuthenticationFailed) = result { + // Expected + } else { + panic!("Expected AuthenticationFailed error"); + } + } + + #[test] + fn test_decrypt_wrong_nonce() { + let data = b"Hello, World!"; + let key = [42u8; 32]; + let nonce1 = [1u8; 12]; + let nonce2 = [2u8; 12]; + + let encrypted = PasswordEncryption::encrypt(data, &key, &nonce1).unwrap(); + + // Try to decrypt with wrong nonce + let result = PasswordEncryption::decrypt(&encrypted, &key, &nonce2); + assert!(result.is_err()); + if let Err(CryptoError::AuthenticationFailed) = result { + // Expected + } else { + panic!("Expected AuthenticationFailed error"); + } + } + + #[test] + fn test_decrypt_corrupted_data() { + let data = b"Hello, World!"; + let key = [42u8; 32]; + let nonce = [1u8; 12]; + + let mut encrypted = PasswordEncryption::encrypt(data, &key, &nonce).unwrap(); + + // Corrupt the data + encrypted[0] ^= 0xFF; + + // Try to decrypt corrupted data + let result = PasswordEncryption::decrypt(&encrypted, &key, &nonce); + assert!(result.is_err()); + if let Err(CryptoError::AuthenticationFailed) = result { + // Expected + } else { + panic!("Expected AuthenticationFailed error"); + } + } + + #[test] + fn test_derive_key_deterministic() { + let password = "test_password"; + let salt = [42u8; 32]; + let params = Argon2Params::default(); + + let key1 = PasswordEncryption::derive_key(password, &salt, ¶ms).unwrap(); + let key2 = PasswordEncryption::derive_key(password, &salt, ¶ms).unwrap(); + + // Same password and salt should produce same key + assert_eq!(key1, key2); + assert_eq!(key1.len(), 32); + } + + #[test] + fn test_derive_key_different_passwords() { + let salt = [42u8; 32]; + let params = Argon2Params::default(); + + let key1 = PasswordEncryption::derive_key("password1", &salt, ¶ms).unwrap(); + let key2 = PasswordEncryption::derive_key("password2", &salt, ¶ms).unwrap(); + + // Different passwords should produce different keys + assert_ne!(key1, key2); + } + + #[test] + fn test_derive_key_different_salts() { + let password = "test_password"; + let salt1 = [42u8; 32]; + let salt2 = [43u8; 32]; + let params = Argon2Params::default(); + + let key1 = PasswordEncryption::derive_key(password, &salt1, ¶ms).unwrap(); + let key2 = PasswordEncryption::derive_key(password, &salt2, ¶ms).unwrap(); + + // Different salts should produce different keys + assert_ne!(key1, key2); + } + + #[test] + fn test_derive_key_empty_password() { + let salt = [42u8; 32]; + let params = Argon2Params::default(); + + let result = PasswordEncryption::derive_key("", &salt, ¶ms); + assert!(result.is_err()); + if let Err(CryptoError::InvalidPassword) = result { + // Expected + } else { + panic!("Expected InvalidPassword error"); + } + } +} + +// Feature: file-encryption, Property 1: Password encryption round-trip +// For any compressed file and any non-empty password, encrypting the file with that password +// and then decrypting with the same password should produce data that decompresses to the +// original content. + +#[cfg(test)] +mod proptests { + use super::*; + use proptest::prelude::*; + + // Use lighter Argon2 parameters for testing to speed up tests + fn test_params() -> Argon2Params { + Argon2Params { + memory_cost: 1024, // 1MB instead of 64MB + time_cost: 1, // 1 iteration instead of 3 + parallelism: 1, // 1 thread instead of 4 + } + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(100))] + + #[test] + fn prop_password_encryption_round_trip( + data in prop::collection::vec(any::(), 0..10000), + password in "[a-zA-Z0-9]{1,100}", + ) { + let salt = PasswordEncryption::generate_salt().unwrap(); + let nonce = PasswordEncryption::generate_nonce().unwrap(); + let params = test_params(); + + // Derive key from password + let key = PasswordEncryption::derive_key(&password, &salt, ¶ms).unwrap(); + + // Encrypt + let encrypted = PasswordEncryption::encrypt(&data, &key, &nonce).unwrap(); + + // Decrypt with same password + let key2 = PasswordEncryption::derive_key(&password, &salt, ¶ms).unwrap(); + let decrypted = PasswordEncryption::decrypt(&encrypted, &key2, &nonce).unwrap(); + + // Should match original + assert_eq!(decrypted, data); + } + + #[test] + fn prop_different_passwords_produce_different_keys( + password1 in "[a-zA-Z0-9]{1,100}", + password2 in "[a-zA-Z0-9]{1,100}", + ) { + prop_assume!(password1 != password2); + + let salt = [42u8; 32]; + let params = test_params(); + + let key1 = PasswordEncryption::derive_key(&password1, &salt, ¶ms).unwrap(); + let key2 = PasswordEncryption::derive_key(&password2, &salt, ¶ms).unwrap(); + + assert_ne!(key1, key2); + } + + #[test] + fn prop_same_password_same_salt_produces_same_key( + password in "[a-zA-Z0-9]{1,100}", + salt in prop::array::uniform32(any::()), + ) { + let params = test_params(); + + let key1 = PasswordEncryption::derive_key(&password, &salt, ¶ms).unwrap(); + let key2 = PasswordEncryption::derive_key(&password, &salt, ¶ms).unwrap(); + + assert_eq!(key1, key2); + } + + // Feature: file-encryption, Property 6: Wrong password authentication failure + // For any password-encrypted file, attempting to decrypt with an incorrect password + // should fail authentication during the AES-GCM decryption process. + + #[test] + fn prop_wrong_password_fails_authentication( + data in prop::collection::vec(any::(), 1..1000), + password1 in "[a-zA-Z0-9]{1,100}", + password2 in "[a-zA-Z0-9]{1,100}", + ) { + prop_assume!(password1 != password2); + + let salt = PasswordEncryption::generate_salt().unwrap(); + let nonce = PasswordEncryption::generate_nonce().unwrap(); + let params = test_params(); + + // Encrypt with password1 + let key1 = PasswordEncryption::derive_key(&password1, &salt, ¶ms).unwrap(); + let encrypted = PasswordEncryption::encrypt(&data, &key1, &nonce).unwrap(); + + // Try to decrypt with password2 (wrong password) + let key2 = PasswordEncryption::derive_key(&password2, &salt, ¶ms).unwrap(); + let result = PasswordEncryption::decrypt(&encrypted, &key2, &nonce); + + // Should fail with authentication error + assert!(result.is_err()); + if let Err(e) = result { + assert!(matches!(e, CryptoError::AuthenticationFailed)); + } + } + + // Feature: file-encryption, Property 9: Random value uniqueness + // For any sequence of encryption operations, generated random values (nonces, symmetric keys, salts) + // should be unique across operations with overwhelming probability. + + #[test] + fn prop_random_salts_are_unique(_seed in any::()) { + // Generate multiple salts + let salts: Vec<[u8; 32]> = (0..100) + .map(|_| PasswordEncryption::generate_salt().unwrap()) + .collect(); + + // Check that all salts are unique + for i in 0..salts.len() { + for j in (i + 1)..salts.len() { + assert_ne!(salts[i], salts[j], "Salts at positions {} and {} are identical", i, j); + } + } + } + + #[test] + fn prop_random_nonces_are_unique(_seed in any::()) { + // Generate multiple nonces + let nonces: Vec<[u8; 12]> = (0..100) + .map(|_| PasswordEncryption::generate_nonce().unwrap()) + .collect(); + + // Check that all nonces are unique + for i in 0..nonces.len() { + for j in (i + 1)..nonces.len() { + assert_ne!(nonces[i], nonces[j], "Nonces at positions {} and {} are identical", i, j); + } + } + } + } +} diff --git a/src/crypto/rsa.rs b/src/crypto/rsa.rs new file mode 100644 index 0000000..7032ea5 --- /dev/null +++ b/src/crypto/rsa.rs @@ -0,0 +1,330 @@ +//! RSA encryption implementation + +use super::{CryptoError, CryptoResult}; +use crate::crypto::keys::read_private_key_pem; +use ring::aead::{ + Aad, BoundKey, Nonce, NonceSequence, OpeningKey, SealingKey, UnboundKey, AES_256_GCM, +}; +use ring::error::Unspecified; +use ring::rand::{SecureRandom, SystemRandom}; +use rsa::Oaep; +use sha2::Sha256; +use std::path::Path; + +/// RSA encryption operations +pub struct RsaEncryption; + +impl RsaEncryption { + /// Generate random symmetric key for AES-256-GCM using cryptographically secure RNG + pub fn generate_symmetric_key() -> CryptoResult<[u8; 32]> { + let rng = SystemRandom::new(); + let mut key = [0u8; 32]; + rng.fill(&mut key).map_err(|_| { + CryptoError::EncryptionFailed("Failed to generate symmetric key".to_string()) + })?; + Ok(key) + } + + /// Generate random nonce using cryptographically secure RNG + pub fn generate_nonce() -> CryptoResult<[u8; 12]> { + let rng = SystemRandom::new(); + let mut nonce = [0u8; 12]; + rng.fill(&mut nonce) + .map_err(|_| CryptoError::EncryptionFailed("Failed to generate nonce".to_string()))?; + Ok(nonce) + } + + /// Encrypt symmetric key with RSA public key using OAEP padding + /// Note: Despite the requirements saying "private key", standard RSA encryption + /// uses the public key to encrypt (so only the private key holder can decrypt). + /// The CLI will accept a public key path for encryption. + pub fn encrypt_symmetric_key( + symmetric_key: &[u8; 32], + public_key_path: &Path, + ) -> CryptoResult> { + // Read and parse public key + use crate::crypto::keys::read_public_key_pem; + let public_key = read_public_key_pem(public_key_path)?; + + // Use OAEP padding with SHA-256 + let padding = Oaep::new::(); + + // Encrypt the symmetric key with the public key + use rand::rngs::OsRng; + let mut rng = OsRng; + let encrypted_key = public_key + .encrypt(&mut rng, padding, symmetric_key) + .map_err(|e| { + CryptoError::RsaError(format!("Failed to encrypt symmetric key: {}", e)) + })?; + + Ok(encrypted_key) + } + + /// Decrypt symmetric key with RSA private key using OAEP padding + /// Note: Despite the requirements saying "public key", standard RSA decryption + /// uses the private key to decrypt (after encryption with the public key). + /// The CLI will accept a private key path for decryption. + pub fn decrypt_symmetric_key( + encrypted_key: &[u8], + private_key_path: &Path, + ) -> CryptoResult<[u8; 32]> { + // Read and parse private key + let private_key = read_private_key_pem(private_key_path)?; + + // Use OAEP padding with SHA-256 + let padding = Oaep::new::(); + + // Decrypt the symmetric key with the private key + let decrypted = private_key.decrypt(padding, encrypted_key).map_err(|e| { + CryptoError::RsaError(format!("Failed to decrypt symmetric key: {}", e)) + })?; + + // Ensure we got exactly 32 bytes + if decrypted.len() != 32 { + return Err(CryptoError::DecryptionFailed(format!( + "Expected 32 bytes, got {}", + decrypted.len() + ))); + } + + let mut key = [0u8; 32]; + key.copy_from_slice(&decrypted); + Ok(key) + } + + /// Encrypt data with AES-256-GCM using symmetric key + pub fn encrypt_data(data: &[u8], key: &[u8; 32], nonce: &[u8; 12]) -> CryptoResult> { + // Create a nonce sequence that returns our nonce once + struct SingleNonce([u8; 12]); + + impl NonceSequence for SingleNonce { + fn advance(&mut self) -> Result { + Nonce::try_assume_unique_for_key(&self.0) + } + } + + // Create sealing key + let unbound_key = UnboundKey::new(&AES_256_GCM, key) + .map_err(|_| CryptoError::EncryptionFailed("Failed to create key".to_string()))?; + let nonce_sequence = SingleNonce(*nonce); + let mut sealing_key = SealingKey::new(unbound_key, nonce_sequence); + + // Prepare data for encryption (ring modifies in place) + let mut in_out = data.to_vec(); + + // Seal (encrypt and authenticate) + sealing_key + .seal_in_place_append_tag(Aad::empty(), &mut in_out) + .map_err(|_| CryptoError::EncryptionFailed("Encryption failed".to_string()))?; + + Ok(in_out) + } + + /// Decrypt data with AES-256-GCM using symmetric key + pub fn decrypt_data( + encrypted_data: &[u8], + key: &[u8; 32], + nonce: &[u8; 12], + ) -> CryptoResult> { + // Create a nonce sequence that returns our nonce once + struct SingleNonce([u8; 12]); + + impl NonceSequence for SingleNonce { + fn advance(&mut self) -> Result { + Nonce::try_assume_unique_for_key(&self.0) + } + } + + // Create opening key + let unbound_key = UnboundKey::new(&AES_256_GCM, key) + .map_err(|_| CryptoError::DecryptionFailed("Failed to create key".to_string()))?; + let nonce_sequence = SingleNonce(*nonce); + let mut opening_key = OpeningKey::new(unbound_key, nonce_sequence); + + // Prepare data for decryption (ring modifies in place) + let mut in_out = encrypted_data.to_vec(); + + // Open (decrypt and verify authentication) + let decrypted = opening_key + .open_in_place(Aad::empty(), &mut in_out) + .map_err(|_| CryptoError::AuthenticationFailed)?; + + Ok(decrypted.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}; + use rsa::RsaPrivateKey; + use std::io::Write; + use tempfile::NamedTempFile; + + pub(super) fn create_test_key_pair() -> (NamedTempFile, NamedTempFile) { + use rand::rngs::OsRng; + let mut rng = OsRng; + let private_key = RsaPrivateKey::new(&mut rng, 2048).unwrap(); + let public_key = rsa::RsaPublicKey::from(&private_key); + + // Write private key to temp file + let mut priv_file = NamedTempFile::new().unwrap(); + let priv_pem = private_key + .to_pkcs8_pem(LineEnding::LF) + .unwrap() + .to_string(); + priv_file.write_all(priv_pem.as_bytes()).unwrap(); + priv_file.flush().unwrap(); + + // Write public key to temp file + let mut pub_file = NamedTempFile::new().unwrap(); + let pub_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap(); + pub_file.write_all(pub_pem.as_bytes()).unwrap(); + pub_file.flush().unwrap(); + + (priv_file, pub_file) + } + + #[test] + fn test_generate_symmetric_key() { + let key1 = RsaEncryption::generate_symmetric_key().unwrap(); + let key2 = RsaEncryption::generate_symmetric_key().unwrap(); + + // Keys should be different + assert_ne!(key1, key2); + assert_eq!(key1.len(), 32); + assert_eq!(key2.len(), 32); + } + + #[test] + fn test_generate_nonce() { + let nonce1 = RsaEncryption::generate_nonce().unwrap(); + let nonce2 = RsaEncryption::generate_nonce().unwrap(); + + // Nonces should be different + assert_ne!(nonce1, nonce2); + assert_eq!(nonce1.len(), 12); + assert_eq!(nonce2.len(), 12); + } + + #[test] + fn test_rsa_symmetric_key_round_trip() { + let (priv_file, pub_file) = create_test_key_pair(); + let symmetric_key = RsaEncryption::generate_symmetric_key().unwrap(); + + // Encrypt with public key + let encrypted = + RsaEncryption::encrypt_symmetric_key(&symmetric_key, pub_file.path()).unwrap(); + + // Decrypt with private key + let decrypted = RsaEncryption::decrypt_symmetric_key(&encrypted, priv_file.path()).unwrap(); + + // Should match original + assert_eq!(decrypted, symmetric_key); + } + + #[test] + fn test_encrypt_decrypt_data() { + let data = b"Hello, RSA World!"; + let key = RsaEncryption::generate_symmetric_key().unwrap(); + let nonce = RsaEncryption::generate_nonce().unwrap(); + + // Encrypt + let encrypted = RsaEncryption::encrypt_data(data, &key, &nonce).unwrap(); + + // Decrypt + let decrypted = RsaEncryption::decrypt_data(&encrypted, &key, &nonce).unwrap(); + + // Should match original + assert_eq!(decrypted, data); + } + + #[test] + fn test_full_rsa_encryption_flow() { + let (priv_file, pub_file) = create_test_key_pair(); + let data = b"Secret message for RSA encryption test"; + + // Generate symmetric key and nonce + let symmetric_key = RsaEncryption::generate_symmetric_key().unwrap(); + let nonce = RsaEncryption::generate_nonce().unwrap(); + + // Encrypt data with symmetric key + let encrypted_data = RsaEncryption::encrypt_data(data, &symmetric_key, &nonce).unwrap(); + + // Encrypt symmetric key with RSA public key + let encrypted_key = + RsaEncryption::encrypt_symmetric_key(&symmetric_key, pub_file.path()).unwrap(); + + // --- Decryption flow --- + + // Decrypt symmetric key with RSA private key + let recovered_key = + RsaEncryption::decrypt_symmetric_key(&encrypted_key, priv_file.path()).unwrap(); + + // Decrypt data with recovered symmetric key + let decrypted_data = + RsaEncryption::decrypt_data(&encrypted_data, &recovered_key, &nonce).unwrap(); + + // Should match original + assert_eq!(decrypted_data, data); + } +} + +// Feature: file-encryption, Property 2: RSA encryption round-trip +// For any compressed file and any valid RSA key pair (public key for encryption, +// private key for decryption), encrypting the file with the public key and then +// decrypting with the corresponding private key should produce data that decompresses +// to the original content. + +#[cfg(test)] +mod proptests { + use super::tests::create_test_key_pair; + use super::*; + use proptest::prelude::*; + + proptest! { + #![proptest_config(ProptestConfig::with_cases(20))] // Reduced from 100 due to RSA key generation cost + + #[test] + fn prop_rsa_encryption_round_trip( + data in prop::collection::vec(any::(), 0..5000), + ) { + let (priv_file, pub_file) = create_test_key_pair(); + + // Generate symmetric key and nonce + let symmetric_key = RsaEncryption::generate_symmetric_key().unwrap(); + let nonce = RsaEncryption::generate_nonce().unwrap(); + + // Encrypt data with symmetric key + let encrypted_data = RsaEncryption::encrypt_data(&data, &symmetric_key, &nonce).unwrap(); + + // Encrypt symmetric key with RSA public key + let encrypted_key = RsaEncryption::encrypt_symmetric_key(&symmetric_key, pub_file.path()).unwrap(); + + // Decrypt symmetric key with RSA private key + let recovered_key = RsaEncryption::decrypt_symmetric_key(&encrypted_key, priv_file.path()).unwrap(); + + // Decrypt data with recovered symmetric key + let decrypted_data = RsaEncryption::decrypt_data(&encrypted_data, &recovered_key, &nonce).unwrap(); + + // Should match original + assert_eq!(decrypted_data, data); + } + + #[test] + fn prop_different_symmetric_keys_produce_different_ciphertext( + data in prop::collection::vec(any::(), 100..1000), + ) { + let nonce = RsaEncryption::generate_nonce().unwrap(); + let key1 = RsaEncryption::generate_symmetric_key().unwrap(); + let key2 = RsaEncryption::generate_symmetric_key().unwrap(); + + let encrypted1 = RsaEncryption::encrypt_data(&data, &key1, &nonce).unwrap(); + let encrypted2 = RsaEncryption::encrypt_data(&data, &key2, &nonce).unwrap(); + + // Different keys should produce different ciphertext + assert_ne!(encrypted1, encrypted2); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index dde5dd3..dd0cca7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod cli; pub mod compressors; pub mod core; +pub mod crypto; pub mod operations; pub mod utils; diff --git a/src/main.rs b/src/main.rs index 29939db..a8faa1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use clap::Parser; mod cli; mod compressors; mod core; +mod crypto; mod operations; mod utils; diff --git a/src/operations/collection.rs b/src/operations/collection.rs index 73cd593..9f0d165 100644 --- a/src/operations/collection.rs +++ b/src/operations/collection.rs @@ -92,6 +92,7 @@ pub fn collect_and_compress( move_to: None, show_output_size: false, force: collection_config.base.force, + encryption: None, // Encryption happens after collection }; // Generate TAR filename diff --git a/src/operations/compound.rs b/src/operations/compound.rs index 657b3e6..85deaa9 100644 --- a/src/operations/compound.rs +++ b/src/operations/compound.rs @@ -6,6 +6,7 @@ use crate::core::config::CompressionConfig; use crate::core::config::TimestampOption; use crate::core::error::JcResult; use crate::core::types::CompoundFormat; +use crate::operations::encrypt; use crate::utils::{debug, info, remove_file_silent}; /// Compress file(s) with compound format (TAR + secondary compression) @@ -28,6 +29,7 @@ pub fn compress_compound( move_to: None, // Don't move intermediate file show_output_size: false, force: config.force, + encryption: None, // Encryption happens after compound compression }; // Remove timestamp to avoid duplication @@ -46,7 +48,13 @@ pub fn compress_compound( } info!("Created compound archive: {}", secondary_output.display()); - Ok(secondary_output) + + // Step 4: Encrypt if encryption is enabled + if let Some(encryption_method) = &config.encryption { + encrypt::encrypt_file(&secondary_output, encryption_method) + } else { + Ok(secondary_output) + } } /// Compress multiple files with compound format @@ -55,8 +63,39 @@ pub fn compress_compound_batch( format: CompoundFormat, config: CompressionConfig, ) -> Vec> { - inputs - .par_iter() - .map(|input| compress_compound(input, format, &config)) - .collect() + // Check if password encryption is used + let has_password_encryption = matches!( + config.encryption, + Some(crate::core::config::EncryptionMethod::Password) + ); + + if has_password_encryption { + // For password encryption, compress all files first, then encrypt with shared password + let compressed: Vec> = inputs + .par_iter() + .map(|input| { + // Compress without encryption first + let mut temp_config = config.clone(); + temp_config.encryption = None; + compress_compound(input, format, &temp_config) + }) + .collect(); + + // Collect successful compressions + let compressed_paths: Vec = + compressed.into_iter().filter_map(|r| r.ok()).collect(); + + // Encrypt all with the same password + if let Some(encryption_method) = &config.encryption { + encrypt::encrypt_files(compressed_paths, encryption_method) + } else { + vec![] + } + } else { + // For RSA or no encryption, process independently + inputs + .par_iter() + .map(|input| compress_compound(input, format, &config)) + .collect() + } } diff --git a/src/operations/compress.rs b/src/operations/compress.rs index 2770ccf..ce05795 100644 --- a/src/operations/compress.rs +++ b/src/operations/compress.rs @@ -5,9 +5,11 @@ use crate::compressors::create_compressor; use crate::core::config::CompressionConfig; use crate::core::error::{JcError, JcResult}; use crate::core::types::CompressionFormat; +use crate::operations::encrypt; use crate::utils::{error, info}; /// Compress a single file +#[allow(dead_code)] pub fn compress_file( input: &PathBuf, format: CompressionFormat, @@ -23,7 +25,14 @@ pub fn compress_file( }); } - compressor.compress(input, config) + let compressed_path = compressor.compress(input, config)?; + + // Encrypt if encryption is enabled + if let Some(encryption_method) = &config.encryption { + encrypt::encrypt_file(&compressed_path, encryption_method) + } else { + Ok(compressed_path) + } } /// Compress multiple files concurrently @@ -34,15 +43,31 @@ pub fn compress_files( ) -> Vec> { info!("Compressing {} files with {}", inputs.len(), format.name()); - // Use rayon for parallel processing - inputs + // Compress files first + let compressed: Vec> = inputs .par_iter() - .map(|input| match compress_file(input, format, &config) { - Ok(output) => Ok(output), - Err(e) => { - error!("Failed to compress {}: {}", input.display(), e); - Err(e) + .map(|input| { + let compressor = create_compressor(format); + if compressor.supports_levels() && !compressor.validate_level(config.level) { + return Err(JcError::InvalidCompressionLevel { + algorithm: compressor.name().to_string(), + level: config.level, + }); } + compressor.compress(input, &config).map_err(|e| { + error!("Failed to compress {}: {}", input.display(), e); + e + }) }) - .collect() + .collect(); + + // If encryption is enabled, encrypt all compressed files + if let Some(encryption_method) = &config.encryption { + let compressed_paths: Vec = + compressed.into_iter().filter_map(|r| r.ok()).collect(); + + encrypt::encrypt_files(compressed_paths, encryption_method) + } else { + compressed + } } diff --git a/src/operations/decompress.rs b/src/operations/decompress.rs index 20c2e66..6e7a396 100644 --- a/src/operations/decompress.rs +++ b/src/operations/decompress.rs @@ -5,8 +5,9 @@ use std::path::{Path, PathBuf}; use crate::compressors::{ detect_format, Bzip2Compressor, GzipCompressor, TarCompressor, XzCompressor, ZipCompressor, }; -use crate::core::config::CompressionConfig; +use crate::core::config::{CompressionConfig, DecompressionConfig}; use crate::core::error::{JcError, JcResult}; +use crate::operations::decrypt; use crate::utils::{create_decompress_temp_dir, debug, error, info, prompt_overwrite}; /// Helper function to decompress in a working directory based on format @@ -42,6 +43,28 @@ fn decompress_in_working_dir( } } +/// Decompress a single file with decryption support +pub fn decompress_file_with_decryption( + input: &PathBuf, + config: &DecompressionConfig, +) -> JcResult { + // First, decrypt if the file is encrypted + let decrypted_path = + decrypt::decrypt_file(input, config.decryption.as_ref(), config.remove_encrypted)?; + + // Then decompress using the standard config + let compression_config = CompressionConfig { + level: 6, + timestamp: crate::core::config::TimestampOption::None, + move_to: config.move_to.clone(), + show_output_size: false, + force: config.force, + encryption: None, + }; + + decompress_file(&decrypted_path, &compression_config) +} + /// Decompress a single file, handling compound formats pub fn decompress_file(input: &PathBuf, config: &CompressionConfig) -> JcResult { // Create a temporary directory for decompression work @@ -204,16 +227,48 @@ pub fn decompress_file(input: &PathBuf, config: &CompressionConfig) -> JcResult< } /// Decompress multiple files concurrently -pub fn decompress_files(inputs: Vec, config: CompressionConfig) -> Vec> { +pub fn decompress_files( + inputs: Vec, + config: CompressionConfig, + decryption_method: Option, + remove_encrypted: bool, +) -> Vec> { info!("Decompressing {} files", inputs.len()); inputs .par_iter() - .map(|input| match decompress_file(input, &config) { - Ok(output) => Ok(output), - Err(e) => { - error!("Failed to decompress {}: {}", input.display(), e); - Err(e) + .map(|input| { + // Check if file is encrypted (has .jcze extension) + let is_encrypted = input + .extension() + .and_then(|s| s.to_str()) + .map(|s| s == "jcze") + .unwrap_or(false); + + if is_encrypted { + // Decrypt first, then decompress + let decompression_config = DecompressionConfig { + move_to: config.move_to.clone(), + force: config.force, + decryption: decryption_method.clone(), + remove_encrypted, + }; + match decompress_file_with_decryption(input, &decompression_config) { + Ok(output) => Ok(output), + Err(e) => { + error!("Failed to decompress {}: {}", input.display(), e); + Err(e) + } + } + } else { + // Normal decompression + match decompress_file(input, &config) { + Ok(output) => Ok(output), + Err(e) => { + error!("Failed to decompress {}: {}", input.display(), e); + Err(e) + } + } } }) .collect() diff --git a/src/operations/decrypt.rs b/src/operations/decrypt.rs new file mode 100644 index 0000000..0fa7576 --- /dev/null +++ b/src/operations/decrypt.rs @@ -0,0 +1,230 @@ +//! Decryption operations for encrypted files + +use crate::core::config::DecryptionMethod; +use crate::core::error::{JcError, JcResult}; +use crate::crypto::{EncryptedContainer, EncryptionMetadata, PasswordEncryption, RsaEncryption}; +use crate::utils::{error, info}; +use rayon::prelude::*; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Prompt user for password securely (without echo) +fn prompt_password() -> JcResult { + use std::io::{self, Write}; + + print!("Enter decryption password: "); + io::stdout().flush()?; + + let password = rpassword::read_password() + .map_err(|e| JcError::Other(format!("Failed to read password: {}", e)))?; + + if password.is_empty() { + return Err(JcError::Other("Password cannot be empty".to_string())); + } + + Ok(password) +} + +/// Check if a file is encrypted by looking for .jcze extension +pub fn is_encrypted_file(path: &Path) -> bool { + path.extension() + .and_then(|s| s.to_str()) + .map(|s| s == "jcze") + .unwrap_or(false) +} + +/// Decrypt a single encrypted file +pub fn decrypt_file( + encrypted_file: &Path, + decryption_method: Option<&DecryptionMethod>, + remove_encrypted: bool, +) -> JcResult { + // Check if file is encrypted + if !is_encrypted_file(encrypted_file) { + // Not encrypted, return as-is + return Ok(encrypted_file.to_path_buf()); + } + + info!("Decrypting file: {}", encrypted_file.display()); + + // Read encrypted container + let container = EncryptedContainer::read_from_file(encrypted_file) + .map_err(|e| JcError::Other(format!("Failed to read encrypted file: {}", e)))?; + + // Decrypt based on container type and provided method + let decrypted_data = match (&container.metadata, decryption_method) { + ( + EncryptionMetadata::Password { + salt, + nonce, + argon2_params, + }, + _, + ) => { + // Password encryption - prompt for password + let password = prompt_password()?; + + // Derive key + let key = PasswordEncryption::derive_key(&password, salt, argon2_params) + .map_err(|e| JcError::Other(format!("Key derivation failed: {}", e)))?; + + // Decrypt + PasswordEncryption::decrypt(&container.encrypted_data, &key, nonce) + .map_err(|e| JcError::Other(format!("Decryption failed: {}", e)))? + } + ( + EncryptionMetadata::Rsa { + encrypted_key, + nonce, + }, + Some(DecryptionMethod::Rsa { private_key_path }), + ) => { + // RSA encryption with provided private key + let symmetric_key = + RsaEncryption::decrypt_symmetric_key(encrypted_key, private_key_path).map_err( + |e| JcError::Other(format!("Failed to decrypt symmetric key: {}", e)), + )?; + + RsaEncryption::decrypt_data(&container.encrypted_data, &symmetric_key, nonce) + .map_err(|e| JcError::Other(format!("Decryption failed: {}", e)))? + } + (EncryptionMetadata::Rsa { .. }, _) => { + return Err(JcError::Other( + "RSA encrypted file requires --decrypt-key option".to_string(), + )); + } + }; + + // Generate output filename by removing .jcze extension + let output_path = encrypted_file.with_extension(""); + + // Write decrypted data + fs::write(&output_path, &decrypted_data)?; + + info!("Decrypted file created: {}", output_path.display()); + + // Remove encrypted file only if requested + if remove_encrypted { + fs::remove_file(encrypted_file)?; + info!("Removed encrypted file: {}", encrypted_file.display()); + } + + Ok(output_path) +} + +/// Decrypt multiple encrypted files in parallel +#[allow(dead_code)] +pub fn decrypt_files( + encrypted_files: Vec, + decryption_method: Option<&DecryptionMethod>, +) -> Vec> { + info!("Decrypting {} files", encrypted_files.len()); + + // Check if any files are password-encrypted + let has_password_encrypted = encrypted_files.iter().any(|f| { + if let Ok(container) = EncryptedContainer::read_from_file(f) { + matches!(container.metadata, EncryptionMetadata::Password { .. }) + } else { + false + } + }); + + if has_password_encrypted { + // Prompt for password once + let password = match prompt_password() { + Ok(p) => p, + Err(e) => { + let err_msg = format!("{}", e); + return encrypted_files + .iter() + .map(|_| Err(JcError::Other(err_msg.clone()))) + .collect(); + } + }; + + // Decrypt all files + encrypted_files + .par_iter() + .map(|file| { + decrypt_file_with_password(file, &password, decryption_method, false).map_err(|e| { + error!("Failed to decrypt {}: {}", file.display(), e); + e + }) + }) + .collect() + } else { + // No password encryption, decrypt independently + encrypted_files + .par_iter() + .map(|file| { + decrypt_file(file, decryption_method, false).map_err(|e| { + error!("Failed to decrypt {}: {}", file.display(), e); + e + }) + }) + .collect() + } +} + +/// Helper function to decrypt with a pre-obtained password +#[allow(dead_code)] +fn decrypt_file_with_password( + encrypted_file: &Path, + password: &str, + decryption_method: Option<&DecryptionMethod>, + remove_encrypted: bool, +) -> JcResult { + if !is_encrypted_file(encrypted_file) { + return Ok(encrypted_file.to_path_buf()); + } + + let container = EncryptedContainer::read_from_file(encrypted_file) + .map_err(|e| JcError::Other(format!("Failed to read encrypted file: {}", e)))?; + + let decrypted_data = match (&container.metadata, decryption_method) { + ( + EncryptionMetadata::Password { + salt, + nonce, + argon2_params, + }, + _, + ) => { + let key = PasswordEncryption::derive_key(password, salt, argon2_params) + .map_err(|e| JcError::Other(format!("Key derivation failed: {}", e)))?; + + PasswordEncryption::decrypt(&container.encrypted_data, &key, nonce) + .map_err(|e| JcError::Other(format!("Decryption failed: {}", e)))? + } + ( + EncryptionMetadata::Rsa { + encrypted_key, + nonce, + }, + Some(DecryptionMethod::Rsa { private_key_path }), + ) => { + let symmetric_key = + RsaEncryption::decrypt_symmetric_key(encrypted_key, private_key_path).map_err( + |e| JcError::Other(format!("Failed to decrypt symmetric key: {}", e)), + )?; + + RsaEncryption::decrypt_data(&container.encrypted_data, &symmetric_key, nonce) + .map_err(|e| JcError::Other(format!("Decryption failed: {}", e)))? + } + (EncryptionMetadata::Rsa { .. }, _) => { + return Err(JcError::Other( + "RSA encrypted file requires --decrypt-key option".to_string(), + )); + } + }; + + let output_path = encrypted_file.with_extension(""); + fs::write(&output_path, &decrypted_data)?; + + // Remove encrypted file only if requested + if remove_encrypted { + fs::remove_file(encrypted_file)?; + } + + Ok(output_path) +} diff --git a/src/operations/encrypt.rs b/src/operations/encrypt.rs new file mode 100644 index 0000000..0163677 --- /dev/null +++ b/src/operations/encrypt.rs @@ -0,0 +1,224 @@ +//! Encryption operations for compressed files + +use crate::core::config::EncryptionMethod; +use crate::core::error::{JcError, JcResult}; +use crate::crypto::{ + Argon2Params, EncryptedContainer, EncryptionMetadata, EncryptionType, PasswordEncryption, + RsaEncryption, +}; +use crate::utils::{error, info}; +use rayon::prelude::*; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Prompt user for password securely (without echo) +fn prompt_password() -> JcResult { + use std::io::{self, Write}; + + print!("Enter encryption password: "); + io::stdout().flush()?; + + // Read password without echo + let password = rpassword::read_password() + .map_err(|e| JcError::Other(format!("Failed to read password: {}", e)))?; + + if password.is_empty() { + return Err(JcError::Other("Password cannot be empty".to_string())); + } + + Ok(password) +} + +/// Encrypt a single compressed file +pub fn encrypt_file( + compressed_file: &Path, + encryption_method: &EncryptionMethod, +) -> JcResult { + info!("Encrypting file: {}", compressed_file.display()); + + // Read the compressed data + let compressed_data = fs::read(compressed_file)?; + + // Encrypt based on method + let (encryption_type, metadata, encrypted_data) = match encryption_method { + EncryptionMethod::Password => { + // Prompt for password + let password = prompt_password()?; + + // Generate salt and nonce + let salt = PasswordEncryption::generate_salt() + .map_err(|e| JcError::Other(format!("Failed to generate salt: {}", e)))?; + let nonce = PasswordEncryption::generate_nonce() + .map_err(|e| JcError::Other(format!("Failed to generate nonce: {}", e)))?; + + // Derive key from password + let params = Argon2Params::default(); + let key = PasswordEncryption::derive_key(&password, &salt, ¶ms) + .map_err(|e| JcError::Other(format!("Key derivation failed: {}", e)))?; + + // Encrypt data + let encrypted = PasswordEncryption::encrypt(&compressed_data, &key, &nonce) + .map_err(|e| JcError::Other(format!("Encryption failed: {}", e)))?; + + let metadata = EncryptionMetadata::Password { + salt, + nonce, + argon2_params: params, + }; + + (EncryptionType::Password, metadata, encrypted) + } + EncryptionMethod::Rsa { public_key_path } => { + // Generate symmetric key and nonce + let symmetric_key = RsaEncryption::generate_symmetric_key() + .map_err(|e| JcError::Other(format!("Failed to generate symmetric key: {}", e)))?; + let nonce = RsaEncryption::generate_nonce() + .map_err(|e| JcError::Other(format!("Failed to generate nonce: {}", e)))?; + + // Encrypt data with symmetric key + let encrypted_data = + RsaEncryption::encrypt_data(&compressed_data, &symmetric_key, &nonce) + .map_err(|e| JcError::Other(format!("Data encryption failed: {}", e)))?; + + // Encrypt symmetric key with RSA public key + let encrypted_key = + RsaEncryption::encrypt_symmetric_key(&symmetric_key, public_key_path) + .map_err(|e| JcError::Other(format!("RSA encryption failed: {}", e)))?; + + let metadata = EncryptionMetadata::Rsa { + encrypted_key, + nonce, + }; + + (EncryptionType::Rsa, metadata, encrypted_data) + } + }; + + // Create encrypted container + let container = EncryptedContainer::new(encryption_type, metadata, encrypted_data); + + // Generate output filename with .jcze extension + let output_path = compressed_file.with_extension(format!( + "{}.jcze", + compressed_file + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("") + )); + + // Write encrypted container + container + .write_to_file(&output_path) + .map_err(|e| JcError::Other(format!("Failed to write encrypted file: {}", e)))?; + + info!("Encrypted file created: {}", output_path.display()); + + // Remove original compressed file + fs::remove_file(compressed_file)?; + + Ok(output_path) +} + +/// Encrypt multiple compressed files in parallel +pub fn encrypt_files( + compressed_files: Vec, + encryption_method: &EncryptionMethod, +) -> Vec> { + info!( + "Encrypting {} files with {}", + compressed_files.len(), + match encryption_method { + EncryptionMethod::Password => "password", + EncryptionMethod::Rsa { .. } => "RSA", + } + ); + + // For password encryption, we need to prompt once and reuse + // For RSA, each file can be encrypted independently + match encryption_method { + EncryptionMethod::Password => { + // Prompt for password once + let password = match prompt_password() { + Ok(p) => p, + Err(e) => { + let err_msg = format!("{}", e); + return compressed_files + .iter() + .map(|_| Err(JcError::Other(err_msg.clone()))) + .collect(); + } + }; + + // Encrypt all files with the same password + compressed_files + .par_iter() + .map(|file| { + encrypt_file_with_password(file, &password).map_err(|e| { + error!("Failed to encrypt {}: {}", file.display(), e); + e + }) + }) + .collect() + } + EncryptionMethod::Rsa { .. } => { + // Each file can be encrypted independently + compressed_files + .par_iter() + .map(|file| { + encrypt_file(file, encryption_method).map_err(|e| { + error!("Failed to encrypt {}: {}", file.display(), e); + e + }) + }) + .collect() + } + } +} + +/// Helper function to encrypt with a pre-obtained password +fn encrypt_file_with_password(compressed_file: &Path, password: &str) -> JcResult { + let compressed_data = fs::read(compressed_file)?; + + // Generate salt and nonce + let salt = PasswordEncryption::generate_salt() + .map_err(|e| JcError::Other(format!("Failed to generate salt: {}", e)))?; + let nonce = PasswordEncryption::generate_nonce() + .map_err(|e| JcError::Other(format!("Failed to generate nonce: {}", e)))?; + + // Derive key from password + let params = Argon2Params::default(); + let key = PasswordEncryption::derive_key(password, &salt, ¶ms) + .map_err(|e| JcError::Other(format!("Key derivation failed: {}", e)))?; + + // Encrypt data + let encrypted = PasswordEncryption::encrypt(&compressed_data, &key, &nonce) + .map_err(|e| JcError::Other(format!("Encryption failed: {}", e)))?; + + let metadata = EncryptionMetadata::Password { + salt, + nonce, + argon2_params: params, + }; + + // Create encrypted container + let container = EncryptedContainer::new(EncryptionType::Password, metadata, encrypted); + + // Generate output filename + let output_path = compressed_file.with_extension(format!( + "{}.jcze", + compressed_file + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("") + )); + + // Write encrypted container + container + .write_to_file(&output_path) + .map_err(|e| JcError::Other(format!("Failed to write encrypted file: {}", e)))?; + + // Remove original compressed file + fs::remove_file(compressed_file)?; + + Ok(output_path) +} diff --git a/src/operations/mod.rs b/src/operations/mod.rs index 42d393d..6be256f 100644 --- a/src/operations/mod.rs +++ b/src/operations/mod.rs @@ -2,6 +2,8 @@ pub mod collection; pub mod compound; pub mod compress; pub mod decompress; +pub mod decrypt; +pub mod encrypt; #[allow(unused_imports)] pub use collection::collect_and_compress; @@ -11,3 +13,7 @@ pub use compound::{compress_compound, compress_compound_batch}; pub use compress::{compress_file, compress_files}; #[allow(unused_imports)] pub use decompress::{decompress_file, decompress_files}; +#[allow(unused_imports)] +pub use decrypt::{decrypt_file, decrypt_files}; +#[allow(unused_imports)] +pub use encrypt::{encrypt_file, encrypt_files};