From 05182e9eaf089e05a6772c06e40effce896f8593 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Fri, 28 Jun 2024 23:13:22 +0800 Subject: [PATCH 01/26] init: initial project structure - Added initial project files and directories - Set up project configuration and initial dependencies - Finish the Servers Selector UI --- .gitignore | 1 + Cargo.lock | 777 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 + src/app.rs | 162 +++++++++++ src/main.rs | 59 ++++ 5 files changed, 1010 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/app.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..441749d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,777 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ratatui" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.13.0", + "lru", + "paste", + "stability", + "strum", + "strum_macros", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "ssh-utils" +version = "0.1.0" +dependencies = [ + "anyhow", + "backtrace", + "clap", + "crossterm", + "ratatui", +] + +[[package]] +name = "stability" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff9eaf853dec4c8802325d8b6d3dffa86cc707fd7a1a4cdbf416e13b061787a" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5fbabedabe362c618c714dbefda9927b5afc8e2a8102f47f081089a9019226" +dependencies = [ + "itertools 0.12.1", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[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.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9022e7a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ssh-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.86" +backtrace = "0.3.73" +clap = "4.5.7" +crossterm = "0.27.0" +ratatui = "0.27.0" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..dd8296a --- /dev/null +++ b/src/app.rs @@ -0,0 +1,162 @@ +use anyhow::Ok; +use anyhow::Result; +use crossterm::event; +use crossterm::event::Event; +use crossterm::event::KeyCode::*; +use crossterm::event::KeyEventKind; +use ratatui::backend::Backend; +use ratatui::buffer::Buffer; +use ratatui::layout::Constraint; +use ratatui::layout::Layout; +use ratatui::layout::Rect; +use ratatui::style::Modifier; +use ratatui::style::Style; +use ratatui::text::Text; +use ratatui::widgets::HighlightSpacing; +use ratatui::widgets::List; +use ratatui::widgets::ListItem; +use ratatui::widgets::ListState; +use ratatui::widgets::StatefulWidget; +use ratatui::widgets::Widget; +use ratatui::Terminal; + +struct ServerItem { + name: String, + address: String, + username: String +} + +struct ServerList { + state: ListState, + items: Vec, + last_selected: Option, +} + +impl ServerList { + fn with_items(items: Vec) -> ServerList { + ServerList { + state: ListState::default(), + items, + last_selected: None, + } + } + + fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } +} + +pub struct App { + server_list: ServerList +} + +impl Widget for &mut App { + fn render(self, area: Rect, buf: &mut Buffer){ + let vertical = Layout::vertical([ + Constraint::Length(1), + Constraint::Min(0) + ]); + let [head_area, body_area] = vertical.areas(area); + self.render_header(head_area, buf); + self.render_servers(body_area, buf); + } +} + +impl App { + fn render_header(&self, area: Rect, buf: &mut Buffer) { + let text = Text::styled(format!(" {:<10} {:<15} {:<20}", "user", "ip", "name"), Style::default().add_modifier(Modifier::BOLD)); + Widget::render(text, area, buf); + } + + fn render_servers(&mut self, area: Rect, buf: &mut Buffer) { + let items: Vec = self.server_list.items.iter().map(|item| { + ListItem::new(format!("{:<10} {:<15} {:<20}", item.username, item.address, item.name)) + }).collect(); + + let items = List::new(items) + .highlight_style( + Style::default() + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::REVERSED) + ) + .highlight_symbol("> ") + .highlight_spacing(HighlightSpacing::Always); + + StatefulWidget::render(&items, area, buf, &mut self.server_list.state); + } +} + +impl App { + pub fn new() -> Result { + if cfg!(debug_assertions) { + let app = Self { + server_list: ServerList::with_items(vec![ + ServerItem { + name: "Aliyun ECS".to_string(), + address: "exmaple.com".to_string(), + username: "admin".to_string() + }, + ServerItem { + name: "AWS lightsail".to_string(), + address: "127.0.0.1".to_string(), + username: "root".to_string() + }, + ServerItem { + name: "My Homelab".to_string(), + address: "192.0.0.1".to_string(), + username: "admin".to_string() + } + ]) + }; + Ok(app) + } else { + todo!() + } + } + + + fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { + terminal.draw(|f| f.render_widget(self, f.size()))?; + Ok(()) + } + + pub fn run(&mut self, mut terminal: &mut Terminal) -> Result<()> { + loop{ + self.draw(&mut terminal)?; + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + Char('q') | Esc => return Ok(()), + Char('j') | Down => self.server_list.next(), + Char('k') | Up => self.server_list.previous(), + _ => {} + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..72d95fc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,59 @@ +mod app; + +use std::{io::{self, Stdout}, panic}; +use app::App; +use backtrace::Backtrace; +use crossterm::terminal::{ + disable_raw_mode, enable_raw_mode + }; +use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport}; +use anyhow::{Context, Ok, Result}; + +fn main() -> Result<()> { + set_panic_handlers()?; + let mut terminal = create_terminal()?; + + setup_terminal(&mut terminal)?; + let app = App::new()?; + run_app(app, &mut terminal)?; + restore_terminal()?; + + Ok(()) +} + +fn run_app(mut app: App, terminal: &mut Terminal>) -> Result<(), anyhow::Error> { + app.run(terminal)?; + Ok(()) +} + +fn setup_terminal(terminal: &mut Terminal>) -> Result<(), anyhow::Error> { + terminal.clear()?; + Ok(()) +} + +fn create_terminal() -> Result>> { + let stdout = io::stdout(); + enable_raw_mode()?; + let terminal_option = TerminalOptions { + //TODO: 设置最大行数 + viewport: Viewport::Inline(10) + }; + Terminal::with_options(CrosstermBackend::new(stdout), terminal_option).context("unable to create terminal") +} + +fn restore_terminal() -> Result<()> { + disable_raw_mode()?; + Ok(()) +} + +// handle all panic here +fn set_panic_handlers() -> Result<()> { + panic::set_hook(Box::new(|e| { + if let Err(e) = disable_raw_mode() { + eprintln!("unable to disable raw mode:\n{e}"); + } + let backtrace = Backtrace::new(); + eprintln!("\nssh-utils was close due to an unexpected panic with the following info:\n\n{:?}\ntrace:\n{:?}", e, backtrace); + })); + Ok(()) +} \ No newline at end of file -- Gitee From 6b74bf08344c35805c1301be574a5d899a1ce1c3 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 29 Jun 2024 01:48:19 +0800 Subject: [PATCH 02/26] feat: restore terminal to status that before exec program - clean program's UI in terminal after shutdown program. --- src/main.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 72d95fc..76994c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,11 +3,12 @@ mod app; use std::{io::{self, Stdout}, panic}; use app::App; use backtrace::Backtrace; -use crossterm::terminal::{ - disable_raw_mode, enable_raw_mode - }; +use crossterm::{cursor::{RestorePosition, SavePosition}, execute, terminal::{ + disable_raw_mode, enable_raw_mode, Clear, ClearType + }}; use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport}; -use anyhow::{Context, Ok, Result}; +use anyhow::{Context, Result}; +use std::io::stdout; fn main() -> Result<()> { set_panic_handlers()?; @@ -32,7 +33,11 @@ fn setup_terminal(terminal: &mut Terminal>) -> Result<( } fn create_terminal() -> Result>> { - let stdout = io::stdout(); + let mut stdout = io::stdout(); + execute!( + stdout, + SavePosition + )?; enable_raw_mode()?; let terminal_option = TerminalOptions { //TODO: 设置最大行数 @@ -41,7 +46,13 @@ fn create_terminal() -> Result>> { Terminal::with_options(CrosstermBackend::new(stdout), terminal_option).context("unable to create terminal") } +// restore terminal to status that before exec program fn restore_terminal() -> Result<()> { + execute!( + stdout(), + RestorePosition, + Clear(ClearType::FromCursorDown) + )?; disable_raw_mode()?; Ok(()) } @@ -49,8 +60,8 @@ fn restore_terminal() -> Result<()> { // handle all panic here fn set_panic_handlers() -> Result<()> { panic::set_hook(Box::new(|e| { - if let Err(e) = disable_raw_mode() { - eprintln!("unable to disable raw mode:\n{e}"); + if let Err(e) = restore_terminal() { + eprintln!("unable to restore terminal:\n{e}"); } let backtrace = Backtrace::new(); eprintln!("\nssh-utils was close due to an unexpected panic with the following info:\n\n{:?}\ntrace:\n{:?}", e, backtrace); -- Gitee From e234d4bf8b9d7e08c530a0b57f6d038e2512cb54 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 1 Jul 2024 11:37:25 +0800 Subject: [PATCH 03/26] feature-wip: exit logical when ctrl + c --- src/app.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app.rs b/src/app.rs index dd8296a..9160c04 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,6 +4,7 @@ use crossterm::event; use crossterm::event::Event; use crossterm::event::KeyCode::*; use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; use ratatui::backend::Backend; use ratatui::buffer::Buffer; use ratatui::layout::Constraint; @@ -153,6 +154,11 @@ impl App { Char('q') | Esc => return Ok(()), Char('j') | Down => self.server_list.next(), Char('k') | Up => self.server_list.previous(), + Char('c') => { + if key.modifiers == KeyModifiers::CONTROL { + return Ok(()) + } + } _ => {} } } -- Gitee From 2f01689c829ee9ac3dc9a68c3763e83aa32f7aa2 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 1 Jul 2024 12:04:05 +0800 Subject: [PATCH 04/26] improvement: more effective panic hook --- src/main.rs | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index 76994c7..47de51b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ mod app; -use std::{io::{self, Stdout}, panic}; +use std::{io::{self, Stdout}, panic::{self, PanicInfo}}; use app::App; -use backtrace::Backtrace; use crossterm::{cursor::{RestorePosition, SavePosition}, execute, terminal::{ disable_raw_mode, enable_raw_mode, Clear, ClearType }}; @@ -11,7 +10,8 @@ use anyhow::{Context, Result}; use std::io::stdout; fn main() -> Result<()> { - set_panic_handlers()?; + // Setup panic hook + panic::set_hook(Box::new(panic_hook)); let mut terminal = create_terminal()?; setup_terminal(&mut terminal)?; @@ -57,14 +57,32 @@ fn restore_terminal() -> Result<()> { Ok(()) } -// handle all panic here -fn set_panic_handlers() -> Result<()> { - panic::set_hook(Box::new(|e| { - if let Err(e) = restore_terminal() { - eprintln!("unable to restore terminal:\n{e}"); - } - let backtrace = Backtrace::new(); - eprintln!("\nssh-utils was close due to an unexpected panic with the following info:\n\n{:?}\ntrace:\n{:?}", e, backtrace); - })); - Ok(()) +/// A panic hook to properly restore the terminal in the case of a panic. +/// Originally based on [spotify-tui's implementation](https://github.com/Rigellute/spotify-tui/blob/master/src/main.rs). +fn panic_hook(panic_info: &PanicInfo<'_>) { + let mut stdout = stdout(); + + let msg = match panic_info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match panic_info.payload().downcast_ref::() { + Some(s) => &s[..], + None => "Box", + }, + }; + + let backtrace = format!("{:?}", backtrace::Backtrace::new()); + + if let Err(e) = restore_terminal() { + eprintln!("unable to restore terminal:\n{e}"); + } + + // Print stack trace. Must be done after! + if let Some(panic_info) = panic_info.location() { + let _ = execute!( + stdout, + crossterm::style::Print(format!( + "application panic: '{msg}', {panic_info}\n\r{backtrace}", + )), + ); + } } \ No newline at end of file -- Gitee From 9e29feaded798b5e4686661fdad32188c5af02cf Mon Sep 17 00:00:00 2001 From: Kurisu Date: Fri, 5 Jul 2024 16:24:58 +0800 Subject: [PATCH 05/26] feature-wip: render tip in footer render `Create (C), Delete (D)` in footer. --- src/app.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9160c04..0ea4833 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,6 +12,7 @@ use ratatui::layout::Layout; use ratatui::layout::Rect; use ratatui::style::Modifier; use ratatui::style::Style; +use ratatui::style::Stylize; use ratatui::text::Text; use ratatui::widgets::HighlightSpacing; use ratatui::widgets::List; @@ -79,11 +80,13 @@ impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer){ let vertical = Layout::vertical([ Constraint::Length(1), - Constraint::Min(0) + Constraint::Min(0), + Constraint::Length(1) ]); - let [head_area, body_area] = vertical.areas(area); + let [head_area, body_area, foot_area] = vertical.areas(area); self.render_header(head_area, buf); self.render_servers(body_area, buf); + self.render_footer(foot_area, buf); } } @@ -93,6 +96,11 @@ impl App { Widget::render(text, area, buf); } + fn render_footer(&self, area: Rect, buf: &mut Buffer) { + let text = Text::from(" Create (C), Delete (D)").dim(); + Widget::render(text, area, buf); + } + fn render_servers(&mut self, area: Rect, buf: &mut Buffer) { let items: Vec = self.server_list.items.iter().map(|item| { ListItem::new(format!("{:<10} {:<15} {:<20}", item.username, item.address, item.name)) -- Gitee From 4c278481ab02d9b27389755c8e2ed652d8e788a7 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 13 Jul 2024 19:47:44 +0800 Subject: [PATCH 06/26] feat: add the page of adding server. --- src/app.rs | 10 +- src/main.rs | 1 + src/widgets/mod.rs | 1 + src/widgets/server_creator.rs | 294 ++++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 src/widgets/mod.rs create mode 100644 src/widgets/server_creator.rs diff --git a/src/app.rs b/src/app.rs index 0ea4833..46da0ff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,6 +22,8 @@ use ratatui::widgets::StatefulWidget; use ratatui::widgets::Widget; use ratatui::Terminal; +use crate::widgets::server_creator::ServerCreator; + struct ServerItem { name: String, address: String, @@ -97,7 +99,7 @@ impl App { } fn render_footer(&self, area: Rect, buf: &mut Buffer) { - let text = Text::from(" Create (C), Delete (D)").dim(); + let text = Text::from(" Add (A), Delete (D), Quit (ESC)").dim(); Widget::render(text, area, buf); } @@ -163,9 +165,15 @@ impl App { Char('j') | Down => self.server_list.next(), Char('k') | Up => self.server_list.previous(), Char('c') => { + // Set this hotkey because of man's habit if key.modifiers == KeyModifiers::CONTROL { return Ok(()) } + }, + Char('a') => { + //添加 server + let mut server_creator = ServerCreator::new(); + server_creator.run(&mut terminal)?; } _ => {} } diff --git a/src/main.rs b/src/main.rs index 47de51b..626ad2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod widgets; use std::{io::{self, Stdout}, panic::{self, PanicInfo}}; use app::App; diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs new file mode 100644 index 0000000..895223b --- /dev/null +++ b/src/widgets/mod.rs @@ -0,0 +1 @@ +pub mod server_creator; \ No newline at end of file diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs new file mode 100644 index 0000000..ab078ee --- /dev/null +++ b/src/widgets/server_creator.rs @@ -0,0 +1,294 @@ +use std::ops::{Add, Sub}; + +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use ratatui::{backend::Backend, + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + style::{Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Paragraph, Widget}, + Terminal}; +use anyhow::Result; + +/// current selected item in form +#[derive(Copy, Clone)] +enum CurrentSelect { + User = 0, + Ip, + Password, + Name, +} + +/// impl Add and Sub for CurrentSelect +impl Add for CurrentSelect { + type Output = Self; + + fn add(self, other: Self) -> Self { + let new_value = (self as isize + other as isize) % 4; + match new_value { + 0 => CurrentSelect::User, + 1 => CurrentSelect::Ip, + 2 => CurrentSelect::Password, + 3 => CurrentSelect::Name, + _ => unreachable!(), + } + } +} + +impl Sub for CurrentSelect { + type Output = Self; + + fn sub(self, other: Self) -> Self { + let new_value = (self as isize - other as isize + 4) % 4; + match new_value { + 0 => CurrentSelect::User, + 1 => CurrentSelect::Ip, + 2 => CurrentSelect::Password, + 3 => CurrentSelect::Name, + _ => unreachable!(), + } + } +} + +impl Add for CurrentSelect { + type Output = Self; + + fn add(self, other: isize) -> Self { + let new_value = (self as isize + other).rem_euclid(4); + match new_value { + 0 => CurrentSelect::User, + 1 => CurrentSelect::Ip, + 2 => CurrentSelect::Password, + 3 => CurrentSelect::Name, + _ => unreachable!(), + } + } +} + +impl Sub for CurrentSelect { + type Output = Self; + + fn sub(self, other: isize) -> Self { + let new_value = (self as isize - other).rem_euclid(4); + match new_value { + 0 => CurrentSelect::User, + 1 => CurrentSelect::Ip, + 2 => CurrentSelect::Password, + 3 => CurrentSelect::Name, + _ => unreachable!(), + } + } +} + +/// App holds the state of the application +pub struct ServerCreator { + /// Current values of the input boxes + input: Vec, + /// Position of cursor in the editor area. + character_index: usize, + /// current selected item + current_select: CurrentSelect, + /// form position + /// used to set cursor + form_position: (u16, u16) +} + +impl Widget for &mut ServerCreator { + fn render(self, area: Rect, buf: &mut Buffer) { + let vertical = Layout::vertical([ + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(1) + ]); + let [head_area, body_area, foot_area] = vertical.areas(area); + self.form_position = (body_area.x, body_area.y); + self.render_header(head_area, buf); + self.render_form(body_area, buf); + self.render_footer(foot_area, buf); + } +} + +impl ServerCreator { + pub fn new() -> Self { + Self { + input: vec![String::new(), String::new(), String::new(), String::new()], + character_index: 0, + current_select: CurrentSelect::User, + form_position: (0,3) + + } + } + + fn render_header(&self, area: Rect, buf: &mut Buffer) { + let text = Text::from("Enter server information below:").yellow(); + Widget::render(text, area, buf); + } + + fn render_footer(&self, area: Rect, buf: &mut Buffer) { + let text = Text::from(" Save (^S), Quit (ESC)").dim(); + Widget::render(text, area, buf); + } + + fn render_form(&self, area: Rect, buf: &mut Buffer) { + // highlight currently selected item + let mut user: Vec = vec![" user:".into(), self.input[CurrentSelect::User as usize].clone().into()]; + let mut ip: Vec = vec![" ip:".into(), self.input[CurrentSelect::Ip as usize].clone().into()]; + let mut password: Vec = vec!["password:".into(), self.input[CurrentSelect::Password as usize].clone().into()]; + let mut name: Vec = vec![" name:".into(), self.input[CurrentSelect::Name as usize].clone().into()]; + + match self.current_select { + CurrentSelect::User => user[0] = Span::styled(" user:", Style::new().bold()), + CurrentSelect::Ip => ip[0] = Span::styled(" ip:", Style::new().bold()), + CurrentSelect::Password => password[0] = Span::styled("password:", Style::new().bold()), + CurrentSelect::Name => name[0] = Span::styled(" name:", Style::new().bold()), + } + + let user_line = Line::from(user); + let ip_line = Line::from(ip); + let password_line = Line::from(password); + let name_line = Line::from(name); + let text = vec![user_line, ip_line, password_line, name_line]; + let form = Paragraph::new(text); + Widget::render(&form, area, buf); + } + + fn move_cursor_left(&mut self) { + let cursor_moved_left = self.character_index.saturating_sub(1); + self.character_index = self.clamp_cursor(cursor_moved_left); + } + + fn move_cursor_right(&mut self) { + let cursor_moved_right = self.character_index.saturating_add(1); + self.character_index = self.clamp_cursor(cursor_moved_right); + } + + fn moveto_current_cursor(&mut self) { + let cursor_position = self.character_index; + self.character_index = self.clamp_cursor(cursor_position); + } + + fn enter_char(&mut self, new_char: char) { + let index = self.byte_index(); + self.input[self.current_select as usize].insert(index, new_char); + self.move_cursor_right(); + } + + /// Returns the byte index based on the character position. + /// + /// Since each character in a string can be contain multiple bytes, it's necessary to calculate + /// the byte index based on the index of the character. + fn byte_index(&mut self) -> usize { + self.input[self.current_select as usize] + .char_indices() + .map(|(i, _)| i) + .nth(self.character_index) + .unwrap_or(self.input[self.current_select as usize].len()) + } + + fn delete_char(&mut self) { + let is_not_cursor_leftmost = self.character_index != 0; + if is_not_cursor_leftmost { + // Method "remove" is not used on the saved text for deleting the selected char. + // Reason: Using remove on String works on bytes instead of the chars. + // Using remove would require special care because of char boundaries. + + let current_index = self.character_index; + let from_left_to_current_index = current_index - 1; + + // Getting all characters before the selected character. + let before_char_to_delete = self.input[self.current_select as usize].chars().take(from_left_to_current_index); + // Getting all characters after selected character. + let after_char_to_delete = self.input[self.current_select as usize].chars().skip(current_index); + + // Put all characters together except the selected one. + // By leaving the selected one out, it is forgotten and therefore deleted. + self.input[self.current_select as usize] = before_char_to_delete.chain(after_char_to_delete).collect(); + self.move_cursor_left(); + } + } + + fn clamp_cursor(&self, new_cursor_pos: usize) -> usize { + new_cursor_pos.clamp(0, self.input[self.current_select as usize].chars().count()) + } + + fn move_next_select_item(&mut self) { + self.current_select = self.current_select + 1; + } + + fn move_pre_select_item(&mut self) { + self.current_select = self.current_select - 1; + } +} + + +impl ServerCreator { + fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { + terminal.draw(|f| { + let character_index = self.character_index as u16; + let form_position = self.form_position; + let cursor_x = form_position.0 + character_index + 9; + let cursor_y = form_position.1 + self.current_select as u16; + + f.render_widget(self, f.size()); + f.set_cursor(cursor_x,cursor_y); + })?; + Ok(()) + } + + pub fn run(&mut self, mut terminal: &mut Terminal) -> Result<()> { + loop{ + self.draw(&mut terminal)?; + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char(to_insert) => { + // Set this hotkey because of man's habit + if to_insert == 'c' { + if key.modifiers == event::KeyModifiers::CONTROL { + return Ok(()) + } + } + self.enter_char(to_insert); + } + KeyCode::Backspace => { + self.delete_char(); + } + KeyCode::Left => { + self.move_cursor_left(); + } + KeyCode::Right => { + self.move_cursor_right(); + } + KeyCode::Esc => { + return Ok(()); + } + KeyCode::Up => { + self.move_pre_select_item(); + self.moveto_current_cursor(); + } + KeyCode::Down | KeyCode::Enter => { + self.move_next_select_item(); + self.moveto_current_cursor(); + } + _ => {} + } + } + } + } + } +} + +#[test] +fn run_widget() -> Result<()> { + crossterm::terminal::enable_raw_mode()?; + let stdout = std::io::stdout(); + crossterm::execute!(std::io::stdout(), crossterm::terminal::EnterAlternateScreen)?; + let backend = ratatui::backend::CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + let mut app = ServerCreator::new(); + + app.run(&mut terminal)?; + crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen)?; + crossterm::terminal::disable_raw_mode()?; + Ok(()) +} \ No newline at end of file -- Gitee From 19ded4e687093bcf597ec479656b964bedfa89a4 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 13 Jul 2024 19:50:20 +0800 Subject: [PATCH 07/26] chore: remove unused prop. --- src/app.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app.rs b/src/app.rs index 46da0ff..07eee92 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,16 +32,14 @@ struct ServerItem { struct ServerList { state: ListState, - items: Vec, - last_selected: Option, + items: Vec } impl ServerList { fn with_items(items: Vec) -> ServerList { ServerList { state: ListState::default(), - items, - last_selected: None, + items } } -- Gitee From 1a568b5559c511dff68accb1c2ce7041a82a680b Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 13 Jul 2024 20:42:23 +0800 Subject: [PATCH 08/26] fix: wrong cursor index in add server page. --- src/widgets/server_creator.rs | 73 ++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs index ab078ee..eb60a14 100644 --- a/src/widgets/server_creator.rs +++ b/src/widgets/server_creator.rs @@ -1,13 +1,6 @@ use std::ops::{Add, Sub}; - use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use ratatui::{backend::Backend, - buffer::Buffer, - layout::{Constraint, Layout, Rect}, - style::{Style, Stylize}, - text::{Line, Span, Text}, - widgets::{Paragraph, Widget}, - Terminal}; +use ratatui::{backend::Backend, buffer::Buffer, layout::{Constraint, Layout, Rect}, style::{Style, Stylize}, text::{Line, Span, Text}, widgets::{Paragraph, Widget}, Frame, Terminal}; use anyhow::Result; /// current selected item in form @@ -87,34 +80,30 @@ pub struct ServerCreator { /// Position of cursor in the editor area. character_index: usize, /// current selected item - current_select: CurrentSelect, - /// form position - /// used to set cursor - form_position: (u16, u16) + current_select: CurrentSelect } -impl Widget for &mut ServerCreator { - fn render(self, area: Rect, buf: &mut Buffer) { - let vertical = Layout::vertical([ - Constraint::Length(1), - Constraint::Min(0), - Constraint::Length(1) - ]); - let [head_area, body_area, foot_area] = vertical.areas(area); - self.form_position = (body_area.x, body_area.y); - self.render_header(head_area, buf); - self.render_form(body_area, buf); - self.render_footer(foot_area, buf); - } -} +// impl Widget for &mut ServerCreator { +// fn render(self, area: Rect, buf: &mut Buffer) { +// let vertical = Layout::vertical([ +// Constraint::Length(1), +// Constraint::Min(0), +// Constraint::Length(1) +// ]); +// let [head_area, body_area, foot_area] = vertical.areas(area); +// self.form_position = (body_area.x, body_area.y); +// self.render_header(head_area, buf); +// self.render_form(body_area, buf); +// self.render_footer(foot_area, buf); +// } +// } impl ServerCreator { pub fn new() -> Self { Self { input: vec![String::new(), String::new(), String::new(), String::new()], character_index: 0, - current_select: CurrentSelect::User, - form_position: (0,3) + current_select: CurrentSelect::User } } @@ -224,13 +213,7 @@ impl ServerCreator { impl ServerCreator { fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { terminal.draw(|f| { - let character_index = self.character_index as u16; - let form_position = self.form_position; - let cursor_x = form_position.0 + character_index + 9; - let cursor_y = form_position.1 + self.current_select as u16; - - f.render_widget(self, f.size()); - f.set_cursor(cursor_x,cursor_y); + ui(f, &self) })?; Ok(()) } @@ -278,6 +261,26 @@ impl ServerCreator { } } +fn ui(f: &mut Frame, server_creator: &ServerCreator) { + let vertical = Layout::vertical([ + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(1) + ]); + let [head_area, body_area, foot_area] = vertical.areas(f.size()); + server_creator.render_header(head_area, f.buffer_mut()); + server_creator.render_form(body_area, f.buffer_mut()); + server_creator.render_footer(foot_area, f.buffer_mut()); + + let character_index = server_creator.character_index as u16; + //due to input character index start at 9 + //eg: "password:" + //so here add 9 + let cursor_x = body_area.x + character_index + 9; + let cursor_y = body_area.y + server_creator.current_select as u16; + f.set_cursor(cursor_x,cursor_y); +} + #[test] fn run_widget() -> Result<()> { crossterm::terminal::enable_raw_mode()?; -- Gitee From f11dd07110d88d193d0f0bd99f8acf4c27a0016e Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sun, 14 Jul 2024 02:49:25 +0800 Subject: [PATCH 09/26] improvement: use * to replace the input password. --- src/widgets/server_creator.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs index eb60a14..3a81837 100644 --- a/src/widgets/server_creator.rs +++ b/src/widgets/server_creator.rs @@ -122,7 +122,10 @@ impl ServerCreator { // highlight currently selected item let mut user: Vec = vec![" user:".into(), self.input[CurrentSelect::User as usize].clone().into()]; let mut ip: Vec = vec![" ip:".into(), self.input[CurrentSelect::Ip as usize].clone().into()]; - let mut password: Vec = vec!["password:".into(), self.input[CurrentSelect::Password as usize].clone().into()]; + // we use * to replace the password + let password_length = self.input[CurrentSelect::Password as usize].len(); + let masked_password: String = "*".repeat(password_length); + let mut password: Vec = vec!["password:".into(), masked_password.into()]; let mut name: Vec = vec![" name:".into(), self.input[CurrentSelect::Name as usize].clone().into()]; match self.current_select { -- Gitee From fb6fb82c2baa1869fcf9a349d6b4a45256053b23 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sun, 14 Jul 2024 02:50:44 +0800 Subject: [PATCH 10/26] improvement: use tab key to move to next item. --- src/widgets/server_creator.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs index 3a81837..3dc2206 100644 --- a/src/widgets/server_creator.rs +++ b/src/widgets/server_creator.rs @@ -252,7 +252,7 @@ impl ServerCreator { self.move_pre_select_item(); self.moveto_current_cursor(); } - KeyCode::Down | KeyCode::Enter => { + KeyCode::Down | KeyCode::Enter | KeyCode::Tab => { self.move_next_select_item(); self.moveto_current_cursor(); } -- Gitee From a0cf30ae984cadf9ca37f91ffc2b397e8e5cebf4 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sat, 27 Jul 2024 02:21:14 +0800 Subject: [PATCH 11/26] chore: add .vscode to gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ea8c4bf..ccb5166 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.vscode \ No newline at end of file -- Gitee From e03d5f8c0099a14cf5df99ca4c20b9f3f0f76705 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sun, 28 Jul 2024 01:41:33 +0800 Subject: [PATCH 12/26] feat: save & read config from file. --- .gitignore | 3 +- Cargo.lock | 170 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + src/app.rs | 34 +++----- src/config/app_config.rs | 74 +++++++++++++++++ src/config/mod.rs | 1 + src/main.rs | 8 +- 7 files changed, 266 insertions(+), 27 deletions(-) create mode 100644 src/config/app_config.rs create mode 100644 src/config/mod.rs diff --git a/.gitignore b/.gitignore index ccb5166..46bbe0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -.vscode \ No newline at end of file +.vscode +.config \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 441749d..83e5090 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,12 +215,50 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.29.0" @@ -243,6 +281,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -279,6 +327,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "lock_api" version = "0.4.12" @@ -346,6 +404,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.3" @@ -423,6 +487,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -447,6 +522,35 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "signal-hook" version = "0.3.17" @@ -491,7 +595,10 @@ dependencies = [ "backtrace", "clap", "crossterm", + "dirs", "ratatui", + "serde", + "toml", ] [[package]] @@ -549,6 +656,60 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -756,6 +917,15 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winnow" +version = "0.6.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.7.34" diff --git a/Cargo.toml b/Cargo.toml index 9022e7a..079017d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,7 @@ anyhow = "1.0.86" backtrace = "0.3.73" clap = "4.5.7" crossterm = "0.27.0" +dirs = "5.0.1" ratatui = "0.27.0" +serde = { version = "1.0", features = ["derive"] } +toml = "0.8.16" diff --git a/src/app.rs b/src/app.rs index 07eee92..2381741 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,6 +22,7 @@ use ratatui::widgets::StatefulWidget; use ratatui::widgets::Widget; use ratatui::Terminal; +use crate::config::app_config::Config; use crate::widgets::server_creator::ServerCreator; struct ServerItem { @@ -120,31 +121,18 @@ impl App { } impl App { - pub fn new() -> Result { - if cfg!(debug_assertions) { - let app = Self { - server_list: ServerList::with_items(vec![ - ServerItem { - name: "Aliyun ECS".to_string(), - address: "exmaple.com".to_string(), - username: "admin".to_string() - }, - ServerItem { - name: "AWS lightsail".to_string(), - address: "127.0.0.1".to_string(), - username: "root".to_string() - }, - ServerItem { - name: "My Homelab".to_string(), - address: "192.0.0.1".to_string(), - username: "admin".to_string() - } - ]) + pub fn new(config: Config) -> Result { + let server_items: Vec = config.servers.into_iter().map(|server| { + ServerItem { + name: server.name, + address: server.ip, + username: server.user, + } + }).collect(); + let app = Self { + server_list: ServerList::with_items(server_items) }; Ok(app) - } else { - todo!() - } } diff --git a/src/config/app_config.rs b/src/config/app_config.rs new file mode 100644 index 0000000..ff2af38 --- /dev/null +++ b/src/config/app_config.rs @@ -0,0 +1,74 @@ +use anyhow::{Context, Ok}; +use serde::Deserialize; +use std::{fs, path::PathBuf}; +use anyhow::Result; + +#[derive(Deserialize, Debug)] +pub struct Server { + pub name: String, + pub ip: String, + pub user: String, +} + +#[derive(Deserialize, Debug, Default)] +pub struct Config { + pub servers: Vec, +} + +/** + check if config file and it's directory exists +*/ +pub fn ensure_config_exists() -> Result<()> { + let mut config_dir: PathBuf = if cfg!(debug_assertions) { + ".".into() // curent running dir + } else { + dirs::home_dir().context("Unable to reach user's home directory.")? + }; + config_dir.push(".config/ssh-utils"); + + // Ensure the config directory exists + if !config_dir.exists() { + fs::create_dir_all(&config_dir).context(format!( + "Failed to create config directory at {:?}", + config_dir + ))?; + } + + // Ensure the config file exists + let config_file_path = config_dir.join("config.toml"); + if !config_file_path.exists() { + fs::File::create(&config_file_path).context(format!( + "Failed to create config file at {:?}", + config_file_path + ))?; + } + + Ok(()) +} + +/** + read toml format config + from "~/.config/ssh-utils/config.toml" +*/ +pub fn read_config() -> Result { + let config_path = if cfg!(debug_assertions) { + ".config/ssh-utils/config.toml".into() + } else { + let mut path = dirs::home_dir().context("Unable to reach user's home directory.")?; + path.push(".config/ssh-utils/config.toml"); + path + }; + + let config_str = fs::read_to_string(&config_path) + .with_context(|| format!("Unable to read ssh-utils' config file at {:?}", config_path))?; + + // Check if the config file content is empty + if config_str.trim().is_empty() { + return Ok(Config::default()); + } + + let config: Config = toml::from_str(&config_str) + .context("Failed to parse ssh-utils' config file.")?; + + Ok(config) +} \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..86144d4 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1 @@ +pub mod app_config; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 626ad2b..072baa2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ mod app; mod widgets; +mod config; use std::{io::{self, Stdout}, panic::{self, PanicInfo}}; use app::App; +use config::app_config; use crossterm::{cursor::{RestorePosition, SavePosition}, execute, terminal::{ disable_raw_mode, enable_raw_mode, Clear, ClearType }}; @@ -13,13 +15,13 @@ use std::io::stdout; fn main() -> Result<()> { // Setup panic hook panic::set_hook(Box::new(panic_hook)); + app_config::ensure_config_exists()?; + let config = app_config::read_config()?; + let app = App::new(config)?; let mut terminal = create_terminal()?; - setup_terminal(&mut terminal)?; - let app = App::new()?; run_app(app, &mut terminal)?; restore_terminal()?; - Ok(()) } -- Gitee From 2774fc2e9618b7c577449f275132be6fe8c291dd Mon Sep 17 00:00:00 2001 From: Kurisu Date: Sun, 28 Jul 2024 20:27:46 +0800 Subject: [PATCH 13/26] feat(Cargo.toml): add dependencies for crypto and ssh-utils functionality feat(src/config/crypto.rs): implement SHA-256 digest derivation, Argon2 key derivation, AES encryption/decryption, and IV generation feat(src/config/mod.rs): restructure config module to include app_config and crypto submodules --- Cargo.lock | 216 +++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 + src/config/crypto.rs | 131 ++++++++++++++++++++++++++ src/config/mod.rs | 3 +- 4 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 src/config/crypto.rs diff --git a/Cargo.lock b/Cargo.lock index 83e5090..40b6829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,18 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "autocfg" version = "1.3.0" @@ -111,12 +123,38 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[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 = "cassowary" version = "0.3.0" @@ -190,6 +228,21 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "constant_time_eq" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crossterm" version = "0.27.0" @@ -215,6 +268,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "5.0.1" @@ -248,6 +321,31 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -404,6 +502,44 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -439,6 +575,18 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.86" @@ -457,6 +605,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ratatui" version = "0.27.0" @@ -498,6 +676,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rust-argon2" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -551,6 +740,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook" version = "0.3.17" @@ -596,8 +796,12 @@ dependencies = [ "clap", "crossterm", "dirs", + "openssl", + "rand", "ratatui", + "rust-argon2", "serde", + "sha2", "toml", ] @@ -710,6 +914,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -744,6 +954,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 079017d..e75041f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,7 @@ dirs = "5.0.1" ratatui = "0.27.0" serde = { version = "1.0", features = ["derive"] } toml = "0.8.16" +rand = "0.8.5" +openssl = "0.10.66" +sha2 = "0.10.8" +rust-argon2 = "2.1.0" diff --git a/src/config/crypto.rs b/src/config/crypto.rs new file mode 100644 index 0000000..1d878a3 --- /dev/null +++ b/src/config/crypto.rs @@ -0,0 +1,131 @@ +use anyhow::{Context, Result}; +use argon2::Config; +use openssl::symm::{Cipher, Crypter, Mode}; +use rand::{thread_rng, Rng}; +use sha2::{Digest, Sha256}; +use std::fs::File; +use std::io::{Read, Write}; + +/** + derive 16 bytes digest from password +*/ +fn derive_sha256_digest(password: &str) -> [u8; 16] { + let mut hasher = Sha256::new(); + hasher.update(password.as_bytes()); + let result = hasher.finalize(); + let mut salt = [0u8; 16]; + salt.copy_from_slice(&result[..16]); + salt +} + +/** + derive 32 bytes hash key from password by argon2 +*/ +fn derive_key_from_password(password: &str, salt: &[u8]) -> Result<[u8; 32]> { + let config = Config::default(); + let key = argon2::hash_raw(password.as_bytes(), salt, &config) + .context("Failed to derive key using Argon2")?; + let mut key_arr = [0u8; 32]; + key_arr.copy_from_slice(&key); + Ok(key_arr) +} + +/** + generate random iv +*/ +fn generate_iv() -> [u8; 16] { + let mut iv = [0u8; 16]; + let mut rng = thread_rng(); + rng.fill(&mut iv); + iv +} + +fn aes_encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { + let cipher = Cipher::aes_256_ctr(); + let mut crypter = + Crypter::new(cipher, Mode::Encrypt, key, Some(iv)).context("Failed to create Crypter")?; + let mut ciphertext = vec![0; data.len() + cipher.block_size()]; + let mut count = crypter + .update(data, &mut ciphertext) + .context("Failed to encrypt data")?; + count += crypter + .finalize(&mut ciphertext[count..]) + .context("Failed to finalize encryption")?; + ciphertext.truncate(count); + Ok(ciphertext) +} + +fn aes_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { + // Step: Decrypt the data using AES-256-CTR + let cipher = Cipher::aes_256_ctr(); + let mut crypter = + Crypter::new(cipher, Mode::Decrypt, key, Some(iv)).context("Failed to create Crypter")?; + let mut plaintext = vec![0; data.len() + cipher.block_size()]; + let mut count = crypter + .update(data, &mut plaintext) + .context("Failed to decrypt data")?; + count += crypter + .finalize(&mut plaintext[count..]) + .context("Failed to finalize decryption")?; + plaintext.truncate(count); + Ok(plaintext) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encryption_decryption() -> Result<()> { + let password = "test_password"; + + // Step 1: Derive 16-byte SHA-256 digest + let salt = derive_sha256_digest(password); + println!("Derived salt: {:?}", salt); + + // Step 2: Derive 32-byte encryption key using argon2 + let encryption_key = derive_key_from_password(password, &salt)?; + println!("Derived encryption key: {:?}", encryption_key); + + // Step 3: Generate 16-byte IV + let iv = generate_iv(); + println!("Generated IV: {:?}", iv); + + // Step 4: Encrypt the string "hello world" + let data = "hello world".as_bytes(); + let encrypted_data = aes_encrypt(&encryption_key, &iv, data)?; + println!("Encrypted data: {:?}", encrypted_data); + + // Save IV and encrypted data to `encrypted_data.bin` + let mut file = + File::create(".config/ssh-utils/encrypted_data.bin").context("Failed to create encrypted_data.bin")?; + file.write_all(&iv).context("Failed to write IV to file")?; + file.write_all(&encrypted_data) + .context("Failed to write encrypted data to file")?; + println!("Encrypted data saved to `encrypted_data.bin`"); + + // Read the encrypted data from 'encrypted_data.bin' + let mut file = + File::open(".config/ssh-utils/encrypted_data.bin").context("Failed to open encrypted_data.bin")?; + let mut iv = [0u8; 16]; + file.read_exact(&mut iv) + .context("Failed to read IV from file")?; + let mut encrypted_data = Vec::new(); + file.read_to_end(&mut encrypted_data) + .context("Failed to read encrypted data from file")?; + + println!("Read IV: {:?}", iv); + println!("Read Encrypted Data: {:?}", encrypted_data); + + // Decrypt the data using the derived key and IV + let decrypted_data = aes_decrypt(&encryption_key, &iv, &encrypted_data)?; + println!( + "Decrypted Data: {}", + String::from_utf8(decrypted_data.clone()).unwrap() + ); + + assert_eq!(String::from_utf8(decrypted_data).unwrap(), "hello world"); + + Ok(()) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 86144d4..cd8f770 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1 +1,2 @@ -pub mod app_config; \ No newline at end of file +pub mod app_config; +pub mod crypto; \ No newline at end of file -- Gitee From c91ffbe54a6815263ff0b0147e6e6a26ce04f570 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 29 Jul 2024 01:48:49 +0800 Subject: [PATCH 14/26] feat: impl vault struct to safely save&read password. --- src/config/app_config.rs | 2 + src/config/app_vault.rs | 112 +++++++++++++++++++++++++++++++++++++++ src/config/crypto.rs | 73 +++---------------------- src/config/mod.rs | 3 +- 4 files changed, 122 insertions(+), 68 deletions(-) create mode 100644 src/config/app_vault.rs diff --git a/src/config/app_config.rs b/src/config/app_config.rs index ff2af38..634a7a1 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -5,6 +5,7 @@ use anyhow::Result; #[derive(Deserialize, Debug)] pub struct Server { + pub id: String, pub name: String, pub ip: String, pub user: String, @@ -17,6 +18,7 @@ pub struct Config { /** check if config file and it's directory exists + if not exists, create them */ pub fn ensure_config_exists() -> Result<()> { let mut config_dir: PathBuf = if cfg!(debug_assertions) { diff --git a/src/config/app_vault.rs b/src/config/app_vault.rs new file mode 100644 index 0000000..2f70aa4 --- /dev/null +++ b/src/config/app_vault.rs @@ -0,0 +1,112 @@ +use anyhow::Result; +use anyhow::{Context, Ok}; +use serde::{Deserialize, Serialize}; +use std::{path::PathBuf}; + +use crate::config::crypto::*; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct Server { + pub id: String, + pub password: String, +} + +#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)] +pub struct Vault { + pub servers: Vec, +} + +/** + check if config file and it's directory exists +*/ +pub fn check_if_password_bin_exists() -> Result { + let mut config_dir: PathBuf = if cfg!(debug_assertions) { + ".".into() // curent running dir + } else { + dirs::home_dir().context("Unable to reach user's home directory.")? + }; + config_dir.push(".config/ssh-utils"); + + // check if the encrypted_data file exists + let config_file_path = config_dir.join("encrypted_data.bin"); + if !config_file_path.exists() { + return Ok(false); + } + + Ok(true) +} + +/** + encrypt vault +*/ +pub fn encrypt_vault(vault: &Vault, password: &str) -> Result> { + // Serialize the Vault object to a string. + let unencrypt_data = toml::to_string(vault).context("Unable to serialize vault to string.")?; + + // Step 1: Derive a 16-byte SHA-256 digest from the password. + let salt = derive_sha256_digest(password); + + // Step 2: Use argon2 to derive a 32-byte encryption key from the password and salt. + let encryption_key = derive_key_from_password(password, &salt)?; + + // Step 3: Generate a 16-byte IV (initialization vector). + let iv = generate_iv(); + + // Step 4: Encrypt the serialized Vault data. + let data = unencrypt_data.as_bytes(); + let encrypted_data = aes_encrypt(&encryption_key, &iv, data)?; + + // Concatenate the IV and encrypted data and return the result. + let mut result = Vec::with_capacity(iv.len() + encrypted_data.len()); + result.extend_from_slice(&iv); + result.extend_from_slice(&encrypted_data); + + Ok(result) +} + + +/** + decrypt vault +*/ +pub fn decrypt_vault(vault: &[u8], password: &str) -> Result { + // Extract the IV and encrypted data. + let (iv, encrypted_data) = vault.split_at(16); + + // Derive the salt from the password. + let salt = derive_sha256_digest(password); + + // Derive the encryption key from the password and salt using Argon2. + let encryption_key = derive_key_from_password(password, &salt)?; + + // Decrypt the data. + let decrypted_data = aes_decrypt(&encryption_key, iv, encrypted_data)?; + + // Convert the decrypted data to a string and parse it into a Vault object. + let decrypted_str = + String::from_utf8(decrypted_data).context("Failed to convert decrypted data to string")?; + let vault: Vault = + toml::from_str(&decrypted_str).context("Failed to parse decrypted data as Vault")?; + + Ok(vault) +} + +/** + test encrypt_vault and decrypt_vault func +*/ +#[test] +fn test_encryption_decryption_vault() -> Result<()> { + let pass_data = r#" +[[servers]] +id = "server1" +password = "secret_password1" + +[[servers]] +id = "server2" +password = "secret_password2" + "#; + let origin_vault: Vault = toml::from_str(pass_data)?; + let encrypt_data = encrypt_vault(&origin_vault,"123")?; + let decrypt_vault = decrypt_vault(&encrypt_data, "123")?; + assert_eq!(origin_vault, decrypt_vault); + Ok(()) +} diff --git a/src/config/crypto.rs b/src/config/crypto.rs index 1d878a3..25817f9 100644 --- a/src/config/crypto.rs +++ b/src/config/crypto.rs @@ -3,13 +3,11 @@ use argon2::Config; use openssl::symm::{Cipher, Crypter, Mode}; use rand::{thread_rng, Rng}; use sha2::{Digest, Sha256}; -use std::fs::File; -use std::io::{Read, Write}; /** derive 16 bytes digest from password */ -fn derive_sha256_digest(password: &str) -> [u8; 16] { +pub fn derive_sha256_digest(password: &str) -> [u8; 16] { let mut hasher = Sha256::new(); hasher.update(password.as_bytes()); let result = hasher.finalize(); @@ -21,7 +19,7 @@ fn derive_sha256_digest(password: &str) -> [u8; 16] { /** derive 32 bytes hash key from password by argon2 */ -fn derive_key_from_password(password: &str, salt: &[u8]) -> Result<[u8; 32]> { +pub fn derive_key_from_password(password: &str, salt: &[u8]) -> Result<[u8; 32]> { let config = Config::default(); let key = argon2::hash_raw(password.as_bytes(), salt, &config) .context("Failed to derive key using Argon2")?; @@ -33,14 +31,14 @@ fn derive_key_from_password(password: &str, salt: &[u8]) -> Result<[u8; 32]> { /** generate random iv */ -fn generate_iv() -> [u8; 16] { +pub fn generate_iv() -> [u8; 16] { let mut iv = [0u8; 16]; let mut rng = thread_rng(); rng.fill(&mut iv); iv } -fn aes_encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { +pub fn aes_encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { let cipher = Cipher::aes_256_ctr(); let mut crypter = Crypter::new(cipher, Mode::Encrypt, key, Some(iv)).context("Failed to create Crypter")?; @@ -55,7 +53,7 @@ fn aes_encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { Ok(ciphertext) } -fn aes_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { +pub fn aes_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { // Step: Decrypt the data using AES-256-CTR let cipher = Cipher::aes_256_ctr(); let mut crypter = @@ -69,63 +67,4 @@ fn aes_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { .context("Failed to finalize decryption")?; plaintext.truncate(count); Ok(plaintext) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_encryption_decryption() -> Result<()> { - let password = "test_password"; - - // Step 1: Derive 16-byte SHA-256 digest - let salt = derive_sha256_digest(password); - println!("Derived salt: {:?}", salt); - - // Step 2: Derive 32-byte encryption key using argon2 - let encryption_key = derive_key_from_password(password, &salt)?; - println!("Derived encryption key: {:?}", encryption_key); - - // Step 3: Generate 16-byte IV - let iv = generate_iv(); - println!("Generated IV: {:?}", iv); - - // Step 4: Encrypt the string "hello world" - let data = "hello world".as_bytes(); - let encrypted_data = aes_encrypt(&encryption_key, &iv, data)?; - println!("Encrypted data: {:?}", encrypted_data); - - // Save IV and encrypted data to `encrypted_data.bin` - let mut file = - File::create(".config/ssh-utils/encrypted_data.bin").context("Failed to create encrypted_data.bin")?; - file.write_all(&iv).context("Failed to write IV to file")?; - file.write_all(&encrypted_data) - .context("Failed to write encrypted data to file")?; - println!("Encrypted data saved to `encrypted_data.bin`"); - - // Read the encrypted data from 'encrypted_data.bin' - let mut file = - File::open(".config/ssh-utils/encrypted_data.bin").context("Failed to open encrypted_data.bin")?; - let mut iv = [0u8; 16]; - file.read_exact(&mut iv) - .context("Failed to read IV from file")?; - let mut encrypted_data = Vec::new(); - file.read_to_end(&mut encrypted_data) - .context("Failed to read encrypted data from file")?; - - println!("Read IV: {:?}", iv); - println!("Read Encrypted Data: {:?}", encrypted_data); - - // Decrypt the data using the derived key and IV - let decrypted_data = aes_decrypt(&encryption_key, &iv, &encrypted_data)?; - println!( - "Decrypted Data: {}", - String::from_utf8(decrypted_data.clone()).unwrap() - ); - - assert_eq!(String::from_utf8(decrypted_data).unwrap(), "hello world"); - - Ok(()) - } -} +} \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index cd8f770..3b5ba15 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,2 +1,3 @@ pub mod app_config; -pub mod crypto; \ No newline at end of file +pub mod crypto; +pub mod app_vault; \ No newline at end of file -- Gitee From ccbeb3b6d8cfa053971de87499b4a325bd181d5d Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 29 Jul 2024 02:55:24 +0800 Subject: [PATCH 15/26] feat: add hmac verification to check if Passphrases correct. --- Cargo.lock | 39 ++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/config/app_vault.rs | 49 ++++++++++++++++++++++++++++++++--------- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40b6829..59e4ca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,6 +286,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -379,6 +380,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -676,6 +686,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "rust-argon2" version = "2.1.0" @@ -796,9 +827,11 @@ dependencies = [ "clap", "crossterm", "dirs", + "hmac", "openssl", "rand", "ratatui", + "rpassword", "rust-argon2", "serde", "sha2", @@ -849,6 +882,12 @@ dependencies = [ "syn", ] +[[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.68" diff --git a/Cargo.toml b/Cargo.toml index e75041f..e2d0591 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ rand = "0.8.5" openssl = "0.10.66" sha2 = "0.10.8" rust-argon2 = "2.1.0" +rpassword = "7.3.1" +hmac = "0.12.1" diff --git a/src/config/app_vault.rs b/src/config/app_vault.rs index 2f70aa4..c334436 100644 --- a/src/config/app_vault.rs +++ b/src/config/app_vault.rs @@ -1,10 +1,13 @@ use anyhow::Result; -use anyhow::{Context, Ok}; +use anyhow::Context; use serde::{Deserialize, Serialize}; -use std::{path::PathBuf}; - +use std::path::PathBuf; +use hmac::{Hmac, Mac}; +use sha2::Sha256; use crate::config::crypto::*; +type HmacSha256 = Hmac; + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Server { pub id: String, @@ -19,7 +22,7 @@ pub struct Vault { /** check if config file and it's directory exists */ -pub fn check_if_password_bin_exists() -> Result { +pub fn check_if_vault_bin_exists() -> Result { let mut config_dir: PathBuf = if cfg!(debug_assertions) { ".".into() // curent running dir } else { @@ -56,21 +59,29 @@ pub fn encrypt_vault(vault: &Vault, password: &str) -> Result> { let data = unencrypt_data.as_bytes(); let encrypted_data = aes_encrypt(&encryption_key, &iv, data)?; - // Concatenate the IV and encrypted data and return the result. - let mut result = Vec::with_capacity(iv.len() + encrypted_data.len()); + // Step 5: Compute HMAC for the IV and encrypted data + let mut mac = HmacSha256::new_from_slice(&encryption_key) + .context("Failed to create HMAC instance")?; + mac.update(&iv); + mac.update(&encrypted_data); + let hmac = mac.finalize().into_bytes(); + + // Concatenate the IV, encrypted data, and HMAC and return the result. + let mut result = Vec::with_capacity(iv.len() + encrypted_data.len() + hmac.len()); result.extend_from_slice(&iv); result.extend_from_slice(&encrypted_data); + result.extend_from_slice(&hmac); Ok(result) } - /** decrypt vault */ pub fn decrypt_vault(vault: &[u8], password: &str) -> Result { - // Extract the IV and encrypted data. - let (iv, encrypted_data) = vault.split_at(16); + // Extract the IV, encrypted data, and HMAC. + let (iv, rest) = vault.split_at(16); + let (encrypted_data, hmac) = rest.split_at(rest.len() - 32); // Derive the salt from the password. let salt = derive_sha256_digest(password); @@ -78,6 +89,13 @@ pub fn decrypt_vault(vault: &[u8], password: &str) -> Result { // Derive the encryption key from the password and salt using Argon2. let encryption_key = derive_key_from_password(password, &salt)?; + // Verify HMAC + let mut mac = HmacSha256::new_from_slice(&encryption_key) + .context("Failed to create HMAC instance")?; + mac.update(iv); + mac.update(encrypted_data); + mac.verify_slice(hmac).context("HMAC verification failed")?; + // Decrypt the data. let decrypted_data = aes_decrypt(&encryption_key, iv, encrypted_data)?; @@ -106,7 +124,18 @@ password = "secret_password2" "#; let origin_vault: Vault = toml::from_str(pass_data)?; let encrypt_data = encrypt_vault(&origin_vault,"123")?; - let decrypt_vault = decrypt_vault(&encrypt_data, "123")?; + let decrypt_vault = match decrypt_vault(&encrypt_data, "123") { + Err(e) => { + if let Some(_) = e.downcast_ref::() { + println!("wrong password"); + return Err(e); + } else { + println!("An unexpected error occurred: {}", e); + return Err(e); + } + }, + Ok(o) => o + }; assert_eq!(origin_vault, decrypt_vault); Ok(()) } -- Gitee From 2b44310d4a78c03e0c4e52f2a05992be0ffef5bb Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 29 Jul 2024 04:14:15 +0800 Subject: [PATCH 16/26] feat: merge salt generate and argon2 encrypt into one func. --- src/config/app_vault.rs | 29 +++++++++-------------------- src/config/crypto.rs | 7 +++++-- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/config/app_vault.rs b/src/config/app_vault.rs index c334436..1e47480 100644 --- a/src/config/app_vault.rs +++ b/src/config/app_vault.rs @@ -42,25 +42,19 @@ pub fn check_if_vault_bin_exists() -> Result { /** encrypt vault */ -pub fn encrypt_vault(vault: &Vault, password: &str) -> Result> { +pub fn encrypt_vault(vault: &Vault, encryption_key: &[u8; 32]) -> Result> { // Serialize the Vault object to a string. let unencrypt_data = toml::to_string(vault).context("Unable to serialize vault to string.")?; - // Step 1: Derive a 16-byte SHA-256 digest from the password. - let salt = derive_sha256_digest(password); - - // Step 2: Use argon2 to derive a 32-byte encryption key from the password and salt. - let encryption_key = derive_key_from_password(password, &salt)?; - // Step 3: Generate a 16-byte IV (initialization vector). let iv = generate_iv(); // Step 4: Encrypt the serialized Vault data. let data = unencrypt_data.as_bytes(); - let encrypted_data = aes_encrypt(&encryption_key, &iv, data)?; + let encrypted_data = aes_encrypt(encryption_key, &iv, data)?; // Step 5: Compute HMAC for the IV and encrypted data - let mut mac = HmacSha256::new_from_slice(&encryption_key) + let mut mac = HmacSha256::new_from_slice(encryption_key) .context("Failed to create HMAC instance")?; mac.update(&iv); mac.update(&encrypted_data); @@ -78,26 +72,20 @@ pub fn encrypt_vault(vault: &Vault, password: &str) -> Result> { /** decrypt vault */ -pub fn decrypt_vault(vault: &[u8], password: &str) -> Result { +pub fn decrypt_vault(vault: &[u8], encryption_key: &[u8; 32]) -> Result { // Extract the IV, encrypted data, and HMAC. let (iv, rest) = vault.split_at(16); let (encrypted_data, hmac) = rest.split_at(rest.len() - 32); - // Derive the salt from the password. - let salt = derive_sha256_digest(password); - - // Derive the encryption key from the password and salt using Argon2. - let encryption_key = derive_key_from_password(password, &salt)?; - // Verify HMAC - let mut mac = HmacSha256::new_from_slice(&encryption_key) + let mut mac = HmacSha256::new_from_slice(encryption_key) .context("Failed to create HMAC instance")?; mac.update(iv); mac.update(encrypted_data); mac.verify_slice(hmac).context("HMAC verification failed")?; // Decrypt the data. - let decrypted_data = aes_decrypt(&encryption_key, iv, encrypted_data)?; + let decrypted_data = aes_decrypt(encryption_key, iv, encrypted_data)?; // Convert the decrypted data to a string and parse it into a Vault object. let decrypted_str = @@ -123,8 +111,9 @@ id = "server2" password = "secret_password2" "#; let origin_vault: Vault = toml::from_str(pass_data)?; - let encrypt_data = encrypt_vault(&origin_vault,"123")?; - let decrypt_vault = match decrypt_vault(&encrypt_data, "123") { + let encryption_key = derive_key_from_password("123")?; + let encrypt_data = encrypt_vault(&origin_vault, &encryption_key)?; + let decrypt_vault = match decrypt_vault(&encrypt_data, &encryption_key) { Err(e) => { if let Some(_) = e.downcast_ref::() { println!("wrong password"); diff --git a/src/config/crypto.rs b/src/config/crypto.rs index 25817f9..2e32ead 100644 --- a/src/config/crypto.rs +++ b/src/config/crypto.rs @@ -19,9 +19,12 @@ pub fn derive_sha256_digest(password: &str) -> [u8; 16] { /** derive 32 bytes hash key from password by argon2 */ -pub fn derive_key_from_password(password: &str, salt: &[u8]) -> Result<[u8; 32]> { +pub fn derive_key_from_password(password: &str) -> Result<[u8; 32]> { + // Step 1: Derive a 16-byte SHA-256 digest from the password. + let salt = derive_sha256_digest(password); + let config = Config::default(); - let key = argon2::hash_raw(password.as_bytes(), salt, &config) + let key = argon2::hash_raw(password.as_bytes(), &salt, &config) .context("Failed to derive key using Argon2")?; let mut key_arr = [0u8; 32]; key_arr.copy_from_slice(&key); -- Gitee From 1e17d63d11e46093dd6e12792408d1433799d735 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 29 Jul 2024 04:16:26 +0800 Subject: [PATCH 17/26] feat: ask passphrase when start app. feat: use zeroize to clear sensitive passphrase data in memory. --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/helper.rs | 17 +++++++++++++++ src/main.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/helper.rs diff --git a/Cargo.lock b/Cargo.lock index 59e4ca4..19c713b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -836,6 +836,7 @@ dependencies = [ "serde", "sha2", "toml", + "zeroize", ] [[package]] @@ -1200,3 +1201,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index e2d0591..09e5611 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ sha2 = "0.10.8" rust-argon2 = "2.1.0" rpassword = "7.3.1" hmac = "0.12.1" +zeroize = "1.8.1" diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 0000000..f7bdbb1 --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,17 @@ +use std::path::PathBuf; +use anyhow::{Context, Result}; + +pub static CONFIG_FILE: &str = "config.toml"; +pub static ENCRYPTED_FILE: &str = "encrypted_data.bin"; + +pub fn get_file_path(file_name: &str) -> Result { + let mut config_dir: PathBuf = if cfg!(debug_assertions) { + ".".into() // current running dir + } else { + dirs::home_dir().context("Unable to reach user's home directory.")? + }; + + config_dir.push(file_name); + let file_path = config_dir.to_str().context("Failed to convert path to string")?.to_string(); + Ok(file_path) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 072baa2..4a8e0ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,27 @@ mod app; mod widgets; mod config; +mod helper; -use std::{io::{self, Stdout}, panic::{self, PanicInfo}}; +use std::{fs::File, io::{self, Read, Stdout}, panic::{self, PanicInfo}}; use app::App; -use config::app_config; +use config::{app_config, app_vault::{check_if_vault_bin_exists, decrypt_vault}, crypto::derive_key_from_password}; use crossterm::{cursor::{RestorePosition, SavePosition}, execute, terminal::{ disable_raw_mode, enable_raw_mode, Clear, ClearType }}; +use helper::{get_file_path, ENCRYPTED_FILE}; use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport}; use anyhow::{Context, Result}; +use zeroize::Zeroize; use std::io::stdout; +use rpassword::read_password; fn main() -> Result<()> { // Setup panic hook panic::set_hook(Box::new(panic_hook)); app_config::ensure_config_exists()?; + let mut encryption_key:Vec = Vec::with_capacity(32); + init_vault(&mut encryption_key)?; let config = app_config::read_config()?; let app = App::new(config)?; let mut terminal = create_terminal()?; @@ -25,6 +31,54 @@ fn main() -> Result<()> { Ok(()) } +fn prompt_passphrase(prompt: &str) -> Result { + print!("{}", prompt); + io::Write::flush(&mut io::stdout())?; + + let passphrase = read_password().unwrap_or_else(|err| { + eprintln!("Error reading password: {}", err); + std::process::exit(1); + }); + + Ok(passphrase) +} + +fn init_vault(encryption_key: &mut Vec) -> Result<(), anyhow::Error> { + let mut passphrase = prompt_passphrase("Enter passphrase (empty for no passphrase): ")?; + let mut confirm_passphrase = prompt_passphrase("Enter the same passphrase again: ")?; + + if passphrase == confirm_passphrase { + if check_if_vault_bin_exists()? { + let mut vault_file = File::open(get_file_path(ENCRYPTED_FILE)?)?; + let mut vault_buf: Vec = Vec::new(); + vault_file.read_to_end(&mut vault_buf)?; + let try_encryption_key: [u8; 32] = derive_key_from_password(passphrase.as_str())?; + // hmac challenge. + match decrypt_vault(&vault_buf, &try_encryption_key) { + Ok(_) => { + encryption_key.extend_from_slice(&try_encryption_key); + }, + Err(e) => { + if let Some(_) = e.downcast_ref::() { + println!("Incorrect passphrase. Please try again."); + return Err(e); + } else { + return Err(anyhow::anyhow!("Failed to decrypt vault: {:?}", e)); + } + } + } + } + } else { + println!("Passphrases do not match. Please ensure both entries are identical."); + std::process::exit(1); + } + // due to the drop!() is not really clear the Passphrases' data in memory. + // so we need zeroize to clear passphrase in memory. + passphrase.zeroize(); + confirm_passphrase.zeroize(); + Ok(()) +} + fn run_app(mut app: App, terminal: &mut Terminal>) -> Result<(), anyhow::Error> { app.run(terminal)?; Ok(()) -- Gitee From 59eeab576fa8d30cf49d8cd12d8b60157287eb9b Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 29 Jul 2024 04:32:17 +0800 Subject: [PATCH 18/26] feat: fallback stdin/stdout when TTY unavliable during typing passphrase. --- src/main.rs | 97 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4a8e0ea..19cb83a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,36 @@ mod app; -mod widgets; mod config; mod helper; +mod widgets; -use std::{fs::File, io::{self, Read, Stdout}, panic::{self, PanicInfo}}; +use anyhow::{Context, Result}; use app::App; -use config::{app_config, app_vault::{check_if_vault_bin_exists, decrypt_vault}, crypto::derive_key_from_password}; -use crossterm::{cursor::{RestorePosition, SavePosition}, execute, terminal::{ - disable_raw_mode, enable_raw_mode, Clear, ClearType - }}; +use config::{ + app_config, + app_vault::{check_if_vault_bin_exists, decrypt_vault, Vault}, + crypto::derive_key_from_password, +}; +use crossterm::{ + cursor::{RestorePosition, SavePosition}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, +}; use helper::{get_file_path, ENCRYPTED_FILE}; use ratatui::{backend::CrosstermBackend, Terminal, TerminalOptions, Viewport}; -use anyhow::{Context, Result}; -use zeroize::Zeroize; use std::io::stdout; -use rpassword::read_password; +use std::{ + fs::File, + io::{self, Read, Stdout}, + panic::{self, PanicInfo}, +}; +use zeroize::Zeroize; fn main() -> Result<()> { // Setup panic hook panic::set_hook(Box::new(panic_hook)); app_config::ensure_config_exists()?; - let mut encryption_key:Vec = Vec::with_capacity(32); - init_vault(&mut encryption_key)?; + let mut encryption_key: Vec = Vec::with_capacity(32); + let vault = init_vault(&mut encryption_key)?; let config = app_config::read_config()?; let app = App::new(config)?; let mut terminal = create_terminal()?; @@ -32,18 +41,24 @@ fn main() -> Result<()> { } fn prompt_passphrase(prompt: &str) -> Result { - print!("{}", prompt); - io::Write::flush(&mut io::stdout())?; - - let passphrase = read_password().unwrap_or_else(|err| { - eprintln!("Error reading password: {}", err); - std::process::exit(1); - }); + let prompt_password = |prompt: &str| { + rpassword::prompt_password(prompt).or_else(|_| { + println!("Cannot use TTY, falling back to stdin/stdout"); + println!("WARNING: Password will be visible on the screen"); + + rpassword::prompt_password_from_bufread( + &mut std::io::BufReader::new(std::io::stdin()), + &mut std::io::stdout(), + prompt, + ) + }) + }; + let passphrase = prompt_password(prompt)?; Ok(passphrase) } -fn init_vault(encryption_key: &mut Vec) -> Result<(), anyhow::Error> { +fn init_vault(encryption_key: &mut Vec) -> Result { let mut passphrase = prompt_passphrase("Enter passphrase (empty for no passphrase): ")?; let mut confirm_passphrase = prompt_passphrase("Enter the same passphrase again: ")?; @@ -54,10 +69,11 @@ fn init_vault(encryption_key: &mut Vec) -> Result<(), anyhow::Error> { vault_file.read_to_end(&mut vault_buf)?; let try_encryption_key: [u8; 32] = derive_key_from_password(passphrase.as_str())?; // hmac challenge. - match decrypt_vault(&vault_buf, &try_encryption_key) { - Ok(_) => { + let vault = match decrypt_vault(&vault_buf, &try_encryption_key) { + Ok(o) => { encryption_key.extend_from_slice(&try_encryption_key); - }, + o + } Err(e) => { if let Some(_) = e.downcast_ref::() { println!("Incorrect passphrase. Please try again."); @@ -66,20 +82,25 @@ fn init_vault(encryption_key: &mut Vec) -> Result<(), anyhow::Error> { return Err(anyhow::anyhow!("Failed to decrypt vault: {:?}", e)); } } - } + }; + // due to the drop!() is not really clear the Passphrases' data in memory. + // so we need zeroize to clear passphrase in memory. + passphrase.zeroize(); + confirm_passphrase.zeroize(); + return Ok(vault); + } else { + Ok(Vault::default()) } } else { println!("Passphrases do not match. Please ensure both entries are identical."); std::process::exit(1); } - // due to the drop!() is not really clear the Passphrases' data in memory. - // so we need zeroize to clear passphrase in memory. - passphrase.zeroize(); - confirm_passphrase.zeroize(); - Ok(()) } -fn run_app(mut app: App, terminal: &mut Terminal>) -> Result<(), anyhow::Error> { +fn run_app( + mut app: App, + terminal: &mut Terminal>, +) -> Result<(), anyhow::Error> { app.run(terminal)?; Ok(()) } @@ -91,25 +112,19 @@ fn setup_terminal(terminal: &mut Terminal>) -> Result<( fn create_terminal() -> Result>> { let mut stdout = io::stdout(); - execute!( - stdout, - SavePosition - )?; + execute!(stdout, SavePosition)?; enable_raw_mode()?; let terminal_option = TerminalOptions { //TODO: 设置最大行数 - viewport: Viewport::Inline(10) + viewport: Viewport::Inline(10), }; - Terminal::with_options(CrosstermBackend::new(stdout), terminal_option).context("unable to create terminal") + Terminal::with_options(CrosstermBackend::new(stdout), terminal_option) + .context("unable to create terminal") } // restore terminal to status that before exec program fn restore_terminal() -> Result<()> { - execute!( - stdout(), - RestorePosition, - Clear(ClearType::FromCursorDown) - )?; + execute!(stdout(), RestorePosition, Clear(ClearType::FromCursorDown))?; disable_raw_mode()?; Ok(()) } @@ -142,4 +157,4 @@ fn panic_hook(panic_info: &PanicInfo<'_>) { )), ); } -} \ No newline at end of file +} -- Gitee From 56b281b3910e7699c3e7e19d50d5c639bd58c221 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 29 Jul 2024 22:32:12 +0800 Subject: [PATCH 19/26] feat: add crud method to add server. --- src/config/app_config.rs | 77 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/src/config/app_config.rs b/src/config/app_config.rs index 634a7a1..d9ce4e3 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -1,9 +1,10 @@ -use anyhow::{Context, Ok}; -use serde::Deserialize; +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; use std::{fs, path::PathBuf}; -use anyhow::Result; -#[derive(Deserialize, Debug)] +use crate::helper::{get_file_path, CONFIG_FILE}; + +#[derive(Serialize, Deserialize, Debug)] pub struct Server { pub id: String, pub name: String, @@ -11,11 +12,75 @@ pub struct Server { pub user: String, } -#[derive(Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default)] pub struct Config { pub servers: Vec, } +impl Config { + /** + Save the current config to the specified file. + */ + pub fn save(&self) -> Result<()> { + let file_path = get_file_path(CONFIG_FILE)?; + let config_str = toml::to_string(self).context("Failed to serialize config to TOML.")?; + + // Ensure the directory exists + let path = PathBuf::from(&file_path); + if let Some(parent) = path.parent() { + if !parent.exists() { + fs::create_dir_all(parent).context(format!( + "Failed to create config directory at {:?}", + parent + ))?; + } + } + + // Write the config to the file + fs::write(&file_path, config_str) + .context(format!("Failed to write config to file at {:?}", file_path))?; + + Ok(()) + } + + /** + Modify a server's information and save the config. + */ + pub fn modify_server(&mut self, id: &str, new_server: Server) -> Result<()> { + if let Some(server) = self.servers.iter_mut().find(|server| server.id == id) { + server.name = new_server.name.clone(); + server.ip = new_server.ip.clone(); + server.user = new_server.user.clone(); + self.save()?; + } else { + return Err(anyhow::anyhow!("Server with id {} not found", id)); + } + Ok(()) + } + + /** + Add a new server and save the config. + */ + pub fn add_server(&mut self, new_server: Server) -> Result<()> { + self.servers.push(new_server); + self.save()?; + Ok(()) + } + + /** + Delete a server by id and save the config. + */ + pub fn delete_server(&mut self, id: &str) -> Result<()> { + if let Some(pos) = self.servers.iter().position(|server| server.id == id) { + self.servers.remove(pos); + self.save()?; + } else { + return Err(anyhow::anyhow!("Server with id {} not found", id)); + } + Ok(()) + } +} + /** check if config file and it's directory exists if not exists, create them @@ -73,4 +138,4 @@ pub fn read_config() -> Result { .context("Failed to parse ssh-utils' config file.")?; Ok(config) -} \ No newline at end of file +} -- Gitee From f8966275ce798301e6f764462fb61dc8a6722aa3 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Mon, 29 Jul 2024 22:42:04 +0800 Subject: [PATCH 20/26] feat: add crud method to add vault item. --- src/config/app_vault.rs | 126 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 2 deletions(-) diff --git a/src/config/app_vault.rs b/src/config/app_vault.rs index 1e47480..3b16451 100644 --- a/src/config/app_vault.rs +++ b/src/config/app_vault.rs @@ -1,10 +1,14 @@ use anyhow::Result; use anyhow::Context; use serde::{Deserialize, Serialize}; +use sha2::Digest; +use std::fs; use std::path::PathBuf; use hmac::{Hmac, Mac}; use sha2::Sha256; use crate::config::crypto::*; +use crate::helper::get_file_path; +use crate::helper::ENCRYPTED_FILE; type HmacSha256 = Hmac; @@ -19,12 +23,62 @@ pub struct Vault { pub servers: Vec, } +impl Vault { + pub fn save(&self, encryption_key: &[u8; 32]) -> Result<()> { + let encrypt_data = encrypt_vault(self, encryption_key)?; + let file_path = get_file_path(ENCRYPTED_FILE)?; + + // Ensure the directory exists + let path = PathBuf::from(&file_path); + if let Some(parent) = path.parent() { + if !parent.exists() { + fs::create_dir_all(parent).context(format!( + "Failed to create config directory at {:?}", + parent + ))?; + } + } + + // Write the encrypted data to the file + fs::write(&file_path, encrypt_data) + .context(format!("Failed to write encrypted data to file at {:?}", file_path))?; + + Ok(()) + } + + pub fn modify_server(&mut self, id: &str, new_server: Server, encryption_key: &[u8; 32]) -> Result<()> { + if let Some(server) = self.servers.iter_mut().find(|server| server.id == id) { + server.password = new_server.password.clone(); + self.save(encryption_key)?; + } else { + return Err(anyhow::anyhow!("Server with id {} not found", id)); + } + Ok(()) + } + + pub fn add_server(&mut self, new_server: Server, encryption_key: &[u8; 32]) -> Result<()> { + self.servers.push(new_server); + self.save(encryption_key)?; + Ok(()) + } + + pub fn delete_server(&mut self, id: &str, encryption_key: &[u8; 32]) -> Result<()> { + if let Some(pos) = self.servers.iter().position(|server| server.id == id) { + self.servers.remove(pos); + self.save(encryption_key)?; + } else { + return Err(anyhow::anyhow!("Server with id {} not found", id)); + } + Ok(()) + } +} + /** - check if config file and it's directory exists + check if config file and its directory exists */ pub fn check_if_vault_bin_exists() -> Result { let mut config_dir: PathBuf = if cfg!(debug_assertions) { - ".".into() // curent running dir + ".".into() // current running dir } else { dirs::home_dir().context("Unable to reach user's home directory.")? }; @@ -96,6 +150,74 @@ pub fn decrypt_vault(vault: &[u8], encryption_key: &[u8; 32]) -> Result { Ok(vault) } +fn derive_iv_from_id(id: &str) -> [u8; 16] { + // Step 1: Hash the id using SHA-256. + let mut hasher = Sha256::new(); + hasher.update(id); + let result = hasher.finalize(); + + // Step 2: Take the first 16 bytes of the hash as the IV. + let mut iv = [0u8; 16]; + iv.copy_from_slice(&result[..16]); + iv +} + +/** + encrypt password to string +*/ +pub fn encrypt_password(id: &str, password: &str, encryption_key: &[u8; 32]) -> Result { + // Derive IV from id. + let iv = derive_iv_from_id(id); + + // Encrypt the password using the provided aes_encrypt function. + let encrypted_data = aes_encrypt(encryption_key, &iv, password.as_bytes())?; + + // Encode the result as a hex string. + let encrypted_hex = hex::encode(encrypted_data); + + Ok(encrypted_hex) +} + +/** + decrypt password to string +*/ +pub fn decrypt_password(id: &str, encrypted_password: &str, encryption_key: &[u8; 32]) -> Result { + // Derive IV from id. + let iv = derive_iv_from_id(id); + + // Decode the encrypted password from hex string. + let encrypted_data = hex::decode(encrypted_password) + .context("Failed to decode hex string")?; + + // Decrypt the password using the provided aes_decrypt function. + let decrypted_data = aes_decrypt(encryption_key, &iv, &encrypted_data)?; + + // Convert the decrypted data to a string. + let decrypted_password = String::from_utf8(decrypted_data) + .context("Failed to convert decrypted data to string")?; + + Ok(decrypted_password) +} + +#[test] +/** + test encrypt_password and decrypt_password func +*/ +fn test_encryption_decryption_password() -> Result<()> { + let id = "550e8400-e29b-41d4-a716-446655440000"; + let password = "my_secure_password"; + let encryption_key = derive_key_from_password("123")?; + + let encrypted_password = encrypt_password(id, password, &encryption_key)?; + println!("Encrypted password: {}", encrypted_password); + + let decrypted_password = decrypt_password(id, &encrypted_password, &encryption_key)?; + println!("Decrypted password: {}", decrypted_password); + + assert_eq!(password,decrypted_password.as_str()); + Ok(()) +} + /** test encrypt_vault and decrypt_vault func */ -- Gitee From 0fe35a0e21eab785be5baef8a3b64bd390c42a4a Mon Sep 17 00:00:00 2001 From: Kurisu Date: Tue, 30 Jul 2024 00:44:54 +0800 Subject: [PATCH 21/26] feat: server creator gui save server information to bin file. --- Cargo.lock | 17 ++++ Cargo.toml | 2 + src/app.rs | 109 +++++++++++++++++-------- src/config/app_config.rs | 14 +++- src/config/app_vault.rs | 7 ++ src/helper.rs | 9 ++ src/main.rs | 18 ++-- src/widgets/server_creator.rs | 149 ++++++++++++++++++++++++++-------- 8 files changed, 251 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19c713b..e8610f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,6 +380,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -827,6 +833,7 @@ dependencies = [ "clap", "crossterm", "dirs", + "hex", "hmac", "openssl", "rand", @@ -836,6 +843,7 @@ dependencies = [ "serde", "sha2", "toml", + "uuid", "zeroize", ] @@ -994,6 +1002,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 09e5611..d6e5bbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,5 @@ rust-argon2 = "2.1.0" rpassword = "7.3.1" hmac = "0.12.1" zeroize = "1.8.1" +hex = "0.4.3" +uuid = { version = "1.0", features = ["v4"] } diff --git a/src/app.rs b/src/app.rs index 2381741..07c130b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,24 +23,27 @@ use ratatui::widgets::Widget; use ratatui::Terminal; use crate::config::app_config::Config; +use crate::config::app_vault::EncryptionKey; +use crate::config::app_vault::Vault; use crate::widgets::server_creator::ServerCreator; struct ServerItem { name: String, address: String, - username: String + username: String, + id: String, } struct ServerList { state: ListState, - items: Vec + items: Vec, } impl ServerList { fn with_items(items: Vec) -> ServerList { ServerList { state: ListState::default(), - items + items, } } @@ -73,16 +76,19 @@ impl ServerList { } } -pub struct App { - server_list: ServerList +pub struct App<'a> { + server_list: ServerList, + vault: &'a mut Vault, + config: &'a mut Config, + encryption_key: EncryptionKey, } -impl Widget for &mut App { - fn render(self, area: Rect, buf: &mut Buffer){ +impl<'a> Widget for &mut App<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { let vertical = Layout::vertical([ Constraint::Length(1), Constraint::Min(0), - Constraint::Length(1) + Constraint::Length(1), ]); let [head_area, body_area, foot_area] = vertical.areas(area); self.render_header(head_area, buf); @@ -91,9 +97,12 @@ impl Widget for &mut App { } } -impl App { +impl<'a> App<'a> { fn render_header(&self, area: Rect, buf: &mut Buffer) { - let text = Text::styled(format!(" {:<10} {:<15} {:<20}", "user", "ip", "name"), Style::default().add_modifier(Modifier::BOLD)); + let text = Text::styled( + format!(" {:<10} {:<15} {:<20}", "user", "ip", "name"), + Style::default().add_modifier(Modifier::BOLD), + ); Widget::render(text, area, buf); } @@ -103,46 +112,64 @@ impl App { } fn render_servers(&mut self, area: Rect, buf: &mut Buffer) { - let items: Vec = self.server_list.items.iter().map(|item| { - ListItem::new(format!("{:<10} {:<15} {:<20}", item.username, item.address, item.name)) - }).collect(); - + let items: Vec = self + .server_list + .items + .iter() + .map(|item| { + ListItem::new(format!( + "{:<10} {:<15} {:<20}", + item.username, item.address, item.name + )) + }) + .collect(); + let items = List::new(items) .highlight_style( Style::default() .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED) + .add_modifier(Modifier::REVERSED), ) .highlight_symbol("> ") .highlight_spacing(HighlightSpacing::Always); - + StatefulWidget::render(&items, area, buf, &mut self.server_list.state); } } -impl App { - pub fn new(config: Config) -> Result { - let server_items: Vec = config.servers.into_iter().map(|server| { - ServerItem { +impl<'a> App<'a> { + pub fn new( + config: &'a mut Config, + vault: &'a mut Vault, + encryption_key: EncryptionKey, + ) -> Result { + let server_items: Vec = config + .servers + .clone() + .into_iter() + .map(|server| ServerItem { + id: server.id, name: server.name, address: server.ip, username: server.user, - } - }).collect(); + }) + .collect(); let app = Self { - server_list: ServerList::with_items(server_items) + server_list: ServerList::with_items(server_items), + vault: vault, + config: config, + encryption_key, }; Ok(app) } - fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { terminal.draw(|f| f.render_widget(self, f.size()))?; Ok(()) } - + pub fn run(&mut self, mut terminal: &mut Terminal) -> Result<()> { - loop{ + loop { self.draw(&mut terminal)?; if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { @@ -153,13 +180,31 @@ impl App { Char('c') => { // Set this hotkey because of man's habit if key.modifiers == KeyModifiers::CONTROL { - return Ok(()) + return Ok(()); } - }, + } Char('a') => { - //添加 server - let mut server_creator = ServerCreator::new(); - server_creator.run(&mut terminal)?; + // Add server + let mut server_creator = + ServerCreator::new(self.vault, self.config, &self.encryption_key); + + if server_creator.run(&mut terminal)? { + // add a new server + // Refresh self.server_list + let server_items: Vec = self + .config + .servers + .clone() + .into_iter() + .map(|server| ServerItem { + id: server.id, + name: server.name, + address: server.ip, + username: server.user, + }) + .collect(); + self.server_list = ServerList::with_items(server_items); + } } _ => {} } @@ -167,4 +212,4 @@ impl App { } } } -} \ No newline at end of file +} diff --git a/src/config/app_config.rs b/src/config/app_config.rs index d9ce4e3..85a378e 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -1,10 +1,11 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; +use uuid::Uuid; use std::{fs, path::PathBuf}; use crate::helper::{get_file_path, CONFIG_FILE}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Server { pub id: String, pub name: String, @@ -12,6 +13,17 @@ pub struct Server { pub user: String, } +impl Server { + pub fn new(name: String, ip: String, user: String) -> Self { + Self { + id: Uuid::new_v4().to_string(), + name, + ip, + user, + } + } +} + #[derive(Serialize, Deserialize, Debug, Default)] pub struct Config { pub servers: Vec, diff --git a/src/config/app_vault.rs b/src/config/app_vault.rs index 3b16451..1f0ff69 100644 --- a/src/config/app_vault.rs +++ b/src/config/app_vault.rs @@ -11,6 +11,7 @@ use crate::helper::get_file_path; use crate::helper::ENCRYPTED_FILE; type HmacSha256 = Hmac; +pub type EncryptionKey = Vec; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Server { @@ -18,6 +19,12 @@ pub struct Server { pub password: String, } +impl Server { + pub fn new(id: String, password: String) -> Self { + Self { id, password } + } +} + #[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq)] pub struct Vault { pub servers: Vec, diff --git a/src/helper.rs b/src/helper.rs index f7bdbb1..590794b 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,6 +1,8 @@ use std::path::PathBuf; use anyhow::{Context, Result}; +use crate::config::app_vault::EncryptionKey; + pub static CONFIG_FILE: &str = "config.toml"; pub static ENCRYPTED_FILE: &str = "encrypted_data.bin"; @@ -11,7 +13,14 @@ pub fn get_file_path(file_name: &str) -> Result { dirs::home_dir().context("Unable to reach user's home directory.")? }; + config_dir.push(".config/ssh-utils"); config_dir.push(file_name); let file_path = config_dir.to_str().context("Failed to convert path to string")?.to_string(); Ok(file_path) +} + +pub fn convert_to_array(vec: &EncryptionKey) -> Result<[u8; 32]> { + let slice = vec.as_slice(); + let array: &[u8; 32] = slice.try_into().context("Failed to convert Vec to [u8; 32]")?; + Ok(*array) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 19cb83a..2e8eb5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use anyhow::{Context, Result}; use app::App; use config::{ app_config, - app_vault::{check_if_vault_bin_exists, decrypt_vault, Vault}, + app_vault::{check_if_vault_bin_exists, decrypt_vault, EncryptionKey, Vault}, crypto::derive_key_from_password, }; use crossterm::{ @@ -29,10 +29,10 @@ fn main() -> Result<()> { // Setup panic hook panic::set_hook(Box::new(panic_hook)); app_config::ensure_config_exists()?; - let mut encryption_key: Vec = Vec::with_capacity(32); - let vault = init_vault(&mut encryption_key)?; - let config = app_config::read_config()?; - let app = App::new(config)?; + let mut encryption_key: EncryptionKey = Vec::with_capacity(32); + let mut vault = init_vault(&mut encryption_key)?; + let mut config = app_config::read_config()?; + let app = App::new(&mut config, &mut vault, encryption_key)?; let mut terminal = create_terminal()?; setup_terminal(&mut terminal)?; run_app(app, &mut terminal)?; @@ -58,16 +58,16 @@ fn prompt_passphrase(prompt: &str) -> Result { Ok(passphrase) } -fn init_vault(encryption_key: &mut Vec) -> Result { +fn init_vault(encryption_key: &mut EncryptionKey) -> Result { let mut passphrase = prompt_passphrase("Enter passphrase (empty for no passphrase): ")?; let mut confirm_passphrase = prompt_passphrase("Enter the same passphrase again: ")?; if passphrase == confirm_passphrase { + let try_encryption_key: [u8; 32] = derive_key_from_password(passphrase.as_str())?; if check_if_vault_bin_exists()? { let mut vault_file = File::open(get_file_path(ENCRYPTED_FILE)?)?; let mut vault_buf: Vec = Vec::new(); vault_file.read_to_end(&mut vault_buf)?; - let try_encryption_key: [u8; 32] = derive_key_from_password(passphrase.as_str())?; // hmac challenge. let vault = match decrypt_vault(&vault_buf, &try_encryption_key) { Ok(o) => { @@ -89,6 +89,10 @@ fn init_vault(encryption_key: &mut Vec) -> Result { confirm_passphrase.zeroize(); return Ok(vault); } else { + // same above + passphrase.zeroize(); + confirm_passphrase.zeroize(); + encryption_key.extend_from_slice(&try_encryption_key); Ok(Vault::default()) } } else { diff --git a/src/widgets/server_creator.rs b/src/widgets/server_creator.rs index 3dc2206..cb6b020 100644 --- a/src/widgets/server_creator.rs +++ b/src/widgets/server_creator.rs @@ -1,7 +1,22 @@ -use std::ops::{Add, Sub}; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use ratatui::{backend::Backend, buffer::Buffer, layout::{Constraint, Layout, Rect}, style::{Style, Stylize}, text::{Line, Span, Text}, widgets::{Paragraph, Widget}, Frame, Terminal}; use anyhow::Result; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use ratatui::{ + backend::Backend, + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + style::{Style, Stylize}, + text::{Line, Span, Text}, + widgets::{Paragraph, Widget}, + Frame, Terminal, +}; +use std::ops::{Add, Sub}; + +use crate::{ + config::{ + app_config::{Config, Server}, + app_vault::{self, encrypt_password, EncryptionKey, Vault}, + }, helper::convert_to_array, +}; /// current selected item in form #[derive(Copy, Clone)] @@ -74,13 +89,17 @@ impl Sub for CurrentSelect { } /// App holds the state of the application -pub struct ServerCreator { +pub struct ServerCreator<'a> { /// Current values of the input boxes input: Vec, /// Position of cursor in the editor area. character_index: usize, /// current selected item - current_select: CurrentSelect + current_select: CurrentSelect, + /// vault + vault: &'a mut Vault, + config: &'a mut Config, + encryption_key: &'a EncryptionKey, } // impl Widget for &mut ServerCreator { @@ -98,13 +117,15 @@ pub struct ServerCreator { // } // } -impl ServerCreator { - pub fn new() -> Self { +impl<'a> ServerCreator<'a> { + pub fn new(vault: &'a mut Vault, config: &'a mut Config, encryption_key: &'a EncryptionKey) -> Self { Self { input: vec![String::new(), String::new(), String::new(), String::new()], character_index: 0, - current_select: CurrentSelect::User - + current_select: CurrentSelect::User, + vault, + config, + encryption_key, } } @@ -120,21 +141,30 @@ impl ServerCreator { fn render_form(&self, area: Rect, buf: &mut Buffer) { // highlight currently selected item - let mut user: Vec = vec![" user:".into(), self.input[CurrentSelect::User as usize].clone().into()]; - let mut ip: Vec = vec![" ip:".into(), self.input[CurrentSelect::Ip as usize].clone().into()]; + let mut user: Vec = vec![ + " user:".into(), + self.input[CurrentSelect::User as usize].clone().into(), + ]; + let mut ip: Vec = vec![ + " ip:".into(), + self.input[CurrentSelect::Ip as usize].clone().into(), + ]; // we use * to replace the password let password_length = self.input[CurrentSelect::Password as usize].len(); let masked_password: String = "*".repeat(password_length); let mut password: Vec = vec!["password:".into(), masked_password.into()]; - let mut name: Vec = vec![" name:".into(), self.input[CurrentSelect::Name as usize].clone().into()]; - + let mut name: Vec = vec![ + " name:".into(), + self.input[CurrentSelect::Name as usize].clone().into(), + ]; + match self.current_select { CurrentSelect::User => user[0] = Span::styled(" user:", Style::new().bold()), CurrentSelect::Ip => ip[0] = Span::styled(" ip:", Style::new().bold()), CurrentSelect::Password => password[0] = Span::styled("password:", Style::new().bold()), CurrentSelect::Name => name[0] = Span::styled(" name:", Style::new().bold()), } - + let user_line = Line::from(user); let ip_line = Line::from(ip); let password_line = Line::from(password); @@ -188,13 +218,18 @@ impl ServerCreator { let from_left_to_current_index = current_index - 1; // Getting all characters before the selected character. - let before_char_to_delete = self.input[self.current_select as usize].chars().take(from_left_to_current_index); + let before_char_to_delete = self.input[self.current_select as usize] + .chars() + .take(from_left_to_current_index); // Getting all characters after selected character. - let after_char_to_delete = self.input[self.current_select as usize].chars().skip(current_index); + let after_char_to_delete = self.input[self.current_select as usize] + .chars() + .skip(current_index); // Put all characters together except the selected one. // By leaving the selected one out, it is forgotten and therefore deleted. - self.input[self.current_select as usize] = before_char_to_delete.chain(after_char_to_delete).collect(); + self.input[self.current_select as usize] = + before_char_to_delete.chain(after_char_to_delete).collect(); self.move_cursor_left(); } } @@ -212,17 +247,19 @@ impl ServerCreator { } } - -impl ServerCreator { +impl<'a> ServerCreator<'a> { fn draw(&mut self, terminal: &mut Terminal) -> Result<()> { - terminal.draw(|f| { - ui(f, &self) - })?; + terminal.draw(|f| ui(f, &self))?; Ok(()) } - pub fn run(&mut self, mut terminal: &mut Terminal) -> Result<()> { - loop{ + /** + * Run and get a result + * true -> add a new server + * false -> cancelled + */ + pub fn run(&mut self, mut terminal: &mut Terminal) -> Result { + loop { self.draw(&mut terminal)?; if let Event::Key(key) = event::read()? { if key.kind == KeyEventKind::Press { @@ -231,7 +268,27 @@ impl ServerCreator { // Set this hotkey because of man's habit if to_insert == 'c' { if key.modifiers == event::KeyModifiers::CONTROL { - return Ok(()) + return Ok(false); + } + } + // Save current server's config + if to_insert == 's' { + if key.modifiers == event::KeyModifiers::CONTROL { + let encryption_key = convert_to_array(&self.encryption_key)?; + let config_server = Server::new( + self.input[CurrentSelect::Name as usize].clone(), + self.input[CurrentSelect::Ip as usize].clone(), + self.input[CurrentSelect::User as usize].clone(), + ); + let passwd = encrypt_password( + &config_server.id, + self.input[CurrentSelect::Password as usize].clone().as_str(), + &encryption_key, + )?; + self.config.add_server(config_server.clone())?; + let vault_server = app_vault::Server::new(config_server.id.clone(),passwd); + self.vault.add_server(vault_server, &encryption_key)?; + return Ok(true); } } self.enter_char(to_insert); @@ -246,7 +303,7 @@ impl ServerCreator { self.move_cursor_right(); } KeyCode::Esc => { - return Ok(()); + return Ok(false); } KeyCode::Up => { self.move_pre_select_item(); @@ -266,22 +323,22 @@ impl ServerCreator { fn ui(f: &mut Frame, server_creator: &ServerCreator) { let vertical = Layout::vertical([ - Constraint::Length(1), - Constraint::Min(0), - Constraint::Length(1) - ]); + Constraint::Length(1), + Constraint::Min(0), + Constraint::Length(1), + ]); let [head_area, body_area, foot_area] = vertical.areas(f.size()); server_creator.render_header(head_area, f.buffer_mut()); server_creator.render_form(body_area, f.buffer_mut()); server_creator.render_footer(foot_area, f.buffer_mut()); - + let character_index = server_creator.character_index as u16; //due to input character index start at 9 //eg: "password:" //so here add 9 let cursor_x = body_area.x + character_index + 9; let cursor_y = body_area.y + server_creator.current_select as u16; - f.set_cursor(cursor_x,cursor_y); + f.set_cursor(cursor_x, cursor_y); } #[test] @@ -291,10 +348,34 @@ fn run_widget() -> Result<()> { crossterm::execute!(std::io::stdout(), crossterm::terminal::EnterAlternateScreen)?; let backend = ratatui::backend::CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let mut app = ServerCreator::new(); + + // get vault start + let mut encryption_key: EncryptionKey = Vec::with_capacity(32); + let mut vault_file = std::fs::File::open(crate::helper::get_file_path(crate::helper::ENCRYPTED_FILE)?)?; + let mut vault_buf: EncryptionKey = Vec::new(); + std::io::Read::read_to_end(&mut vault_file, &mut vault_buf)?; + let try_encryption_key: [u8; 32] = crate::config::crypto::derive_key_from_password("123")?; + // hmac challenge. + let mut vault = match crate::config::app_vault::decrypt_vault(&vault_buf, &try_encryption_key) { + Ok(o) => { + encryption_key.extend_from_slice(&try_encryption_key); + o + } + Err(e) => { + if let Some(_) = e.downcast_ref::() { + println!("Incorrect passphrase. Please try again."); + return Err(e); + } else { + return Err(anyhow::anyhow!("Failed to decrypt vault: {:?}", e)); + } + } + }; + // get vault end + let mut config = crate::config::app_config::read_config()?; + let mut app = ServerCreator::new(&mut vault, &mut config, &encryption_key); app.run(&mut terminal)?; crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen)?; crossterm::terminal::disable_raw_mode()?; Ok(()) -} \ No newline at end of file +} -- Gitee From da1dd4359241e275e0a95d2f8f07e0e104acd6cd Mon Sep 17 00:00:00 2001 From: Kurisu Date: Tue, 30 Jul 2024 01:03:41 +0800 Subject: [PATCH 22/26] feat: max 3 Attempts to check hmac challenge. --- src/main.rs | 55 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2e8eb5c..c06f59e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,48 +59,61 @@ fn prompt_passphrase(prompt: &str) -> Result { } fn init_vault(encryption_key: &mut EncryptionKey) -> Result { - let mut passphrase = prompt_passphrase("Enter passphrase (empty for no passphrase): ")?; - let mut confirm_passphrase = prompt_passphrase("Enter the same passphrase again: ")?; + if check_if_vault_bin_exists()? { + for attempt in 1..=3 { + let prompt_message = if attempt == 1 { + "Enter passphrase: ".to_string() + } else { + format!("Enter passphrase (Attempt {} of 3): ", attempt) + }; - if passphrase == confirm_passphrase { - let try_encryption_key: [u8; 32] = derive_key_from_password(passphrase.as_str())?; - if check_if_vault_bin_exists()? { + let mut passphrase = prompt_passphrase(&prompt_message)?; + let try_encryption_key: [u8; 32] = derive_key_from_password(passphrase.as_str())?; let mut vault_file = File::open(get_file_path(ENCRYPTED_FILE)?)?; let mut vault_buf: Vec = Vec::new(); vault_file.read_to_end(&mut vault_buf)?; + // hmac challenge. - let vault = match decrypt_vault(&vault_buf, &try_encryption_key) { - Ok(o) => { + match decrypt_vault(&vault_buf, &try_encryption_key) { + Ok(vault) => { encryption_key.extend_from_slice(&try_encryption_key); - o + // due to the drop!() is not really clear the Passphrases' data in memory. + // so we use zeroize to clear passphrase in memory. + passphrase.zeroize(); + return Ok(vault); } Err(e) => { + passphrase.zeroize(); if let Some(_) = e.downcast_ref::() { println!("Incorrect passphrase. Please try again."); - return Err(e); + if attempt == 3 { + println!("Maximum attempts reached. Exiting."); + std::process::exit(1); + } } else { return Err(anyhow::anyhow!("Failed to decrypt vault: {:?}", e)); } } - }; - // due to the drop!() is not really clear the Passphrases' data in memory. - // so we need zeroize to clear passphrase in memory. - passphrase.zeroize(); - confirm_passphrase.zeroize(); - return Ok(vault); - } else { - // same above + } + } + } else { + let mut passphrase = prompt_passphrase("Enter passphrase (empty for no passphrase): ")?; + let mut confirm_passphrase = prompt_passphrase("Enter the same passphrase again: ")?; + if passphrase == confirm_passphrase { + let try_encryption_key: [u8; 32] = derive_key_from_password(passphrase.as_str())?; passphrase.zeroize(); confirm_passphrase.zeroize(); encryption_key.extend_from_slice(&try_encryption_key); - Ok(Vault::default()) + return Ok(Vault::default()); + } else { + println!("Passphrases do not match. Please ensure both entries are identical."); + std::process::exit(1); } - } else { - println!("Passphrases do not match. Please ensure both entries are identical."); - std::process::exit(1); } + unreachable!() } + fn run_app( mut app: App, terminal: &mut Terminal>, -- Gitee From acf36fdcfc12072d06a89502bcbab8d1a0640d10 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Tue, 30 Jul 2024 01:20:36 +0800 Subject: [PATCH 23/26] feat: we use owasp's recommand argon2id config. ref: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id --- src/config/crypto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/crypto.rs b/src/config/crypto.rs index 2e32ead..6c3219f 100644 --- a/src/config/crypto.rs +++ b/src/config/crypto.rs @@ -23,7 +23,7 @@ pub fn derive_key_from_password(password: &str) -> Result<[u8; 32]> { // Step 1: Derive a 16-byte SHA-256 digest from the password. let salt = derive_sha256_digest(password); - let config = Config::default(); + let config = Config::owasp3(); let key = argon2::hash_raw(password.as_bytes(), &salt, &config) .context("Failed to derive key using Argon2")?; let mut key_arr = [0u8; 32]; -- Gitee From 582f43c7bac9c06b5787d31424f8e76fda6f652d Mon Sep 17 00:00:00 2001 From: Kurisu Date: Tue, 30 Jul 2024 01:36:36 +0800 Subject: [PATCH 24/26] feat: create empty vault if encrypted_data.bin not exist. --- src/config/app_vault.rs | 6 ++++++ src/main.rs | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/config/app_vault.rs b/src/config/app_vault.rs index 1f0ff69..8c582de 100644 --- a/src/config/app_vault.rs +++ b/src/config/app_vault.rs @@ -151,6 +151,12 @@ pub fn decrypt_vault(vault: &[u8], encryption_key: &[u8; 32]) -> Result { // Convert the decrypted data to a string and parse it into a Vault object. let decrypted_str = String::from_utf8(decrypted_data).context("Failed to convert decrypted data to string")?; + + // If decrypted_str is blank, return a default Vault. + if decrypted_str.trim().is_empty() { + return Ok(Vault::default()); + } + let vault: Vault = toml::from_str(&decrypted_str).context("Failed to parse decrypted data as Vault")?; diff --git a/src/main.rs b/src/main.rs index c06f59e..3a701ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,6 +104,8 @@ fn init_vault(encryption_key: &mut EncryptionKey) -> Result Date: Tue, 30 Jul 2024 02:58:46 +0800 Subject: [PATCH 25/26] feat: add unit tests for crypto related methods. --- src/config/crypto.rs | 128 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/src/config/crypto.rs b/src/config/crypto.rs index 6c3219f..a37b891 100644 --- a/src/config/crypto.rs +++ b/src/config/crypto.rs @@ -70,4 +70,132 @@ pub fn aes_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result> { .context("Failed to finalize decryption")?; plaintext.truncate(count); Ok(plaintext) +} + +#[cfg(test)] +mod tests { + use std::{fs, process::Command}; + + use super::*; + + #[test] + fn test_derive_key_from_password() { + let password = "super_secret_password"; + let salt = b"super_secret_password"; + // The expected key in hexadecimal format is obtained from the command: + // echo -n "super_secret_password" | ./argon2 super_secret_password -t 3 -k 12288 -p 1 -id -l 32 -r + // Since the salt in the command cannot be passed as a byte array, we have defined a custom salt + // Copy the logic of derive_key_from_password() for testing + let expected_key_hex = "66e5467d6adc707c5fe42c2516de285204f4ce590612e58eddaab21b763aaca2"; + let expected_key = hex::decode(expected_key_hex).expect("Decoding failed"); + + let derived_key_result = derive_key_from_password(password); + + let config = Config::owasp3(); + let key = argon2::hash_raw(password.as_bytes(), salt, &config).unwrap(); + + // Check if the result is Ok + assert!(derived_key_result.is_ok()); + + let derived_key = derived_key_result.unwrap(); + + // Check if the length is 32 bytes + assert_eq!(derived_key.len(), 32); + + // Check if the key is non-zero + assert!(derived_key.iter().any(|&byte| byte != 0)); + + // Check if the generated key matches the expected value + assert_eq!(key, expected_key); + } + + #[test] + fn test_derive_sha256_digest() { + let password = "super_secret_password"; + + // Use Rust to execute the command line to get the expected digest value + let output = std::process::Command::new("sh") + .arg("-c") + .arg(format!( + "echo -n \"{}\" | sha256sum | awk '{{print $1}}' | cut -c 1-32", + password + )) + .output() + .expect("Failed to execute command"); + + let expected_digest_hex = String::from_utf8(output.stdout) + .expect("Failed to convert output to string") + .trim() + .to_string(); + + let expected_digest = hex::decode(expected_digest_hex).expect("Decoding failed"); + + let derived_digest = derive_sha256_digest(password); + + // Check if the length is 16 bytes + assert_eq!(derived_digest.len(), 16); + + // Check if the generated digest matches the expected value + assert_eq!(derived_digest, expected_digest.as_slice()); + } + + #[test] + fn test_aes_encrypt_decrypt() { + let key = b"01234567890123456789012345678901"; // A 32-byte key + let iv = b"0123456789012345"; // A 16-byte initialization vector + let data = b"Hello, AES encryption!"; + + // Write data to a temporary file + let data_file_path = "data.txt"; + let mut file = fs::File::create(data_file_path).expect("Failed to create file"); + std::io::Write::write_all(&mut file, data).expect("Failed to write data to file"); + + // Use OpenSSL to generate the expected encrypted value + let expected_encrypted_output = Command::new("openssl") + .arg("enc") + .arg("-aes-256-ctr") + .arg("-in") + .arg(data_file_path) + .arg("-K") + .arg(hex::encode(key)) + .arg("-iv") + .arg(hex::encode(iv)) + .output() + .expect("Failed to execute openssl command"); + let expected_encrypted_data = expected_encrypted_output.stdout; + + // Test aes_encrypt + let encrypted_data = aes_encrypt(key, iv, data).expect("Failed to encrypt data"); + assert!(!encrypted_data.is_empty(), "Encrypted data should not be empty"); + assert_eq!(encrypted_data, expected_encrypted_data, "Encrypted data should match the expected value"); + + // Write encrypted_data to a temporary file + let encrypted_file_path = "encrypted_data.bin"; + let mut encrypted_file = fs::File::create(encrypted_file_path).expect("Failed to create file"); + std::io::Write::write_all(&mut encrypted_file, &encrypted_data).expect("Failed to write encrypted data to file"); + + // Use OpenSSL to generate the expected decrypted value + let expected_decrypted_output = Command::new("openssl") + .arg("enc") + .arg("-d") + .arg("-aes-256-ctr") + .arg("-in") + .arg(encrypted_file_path) + .arg("-K") + .arg(hex::encode(key)) + .arg("-iv") + .arg(hex::encode(iv)) + .output() + .expect("Failed to execute openssl command"); + let expected_decrypted_data = expected_decrypted_output.stdout; + + // Test aes_decrypt + let decrypted_data = aes_decrypt(key, iv, &encrypted_data).expect("Failed to decrypt data"); + assert_eq!(decrypted_data, data, "Decrypted data should match original data"); + assert_eq!(decrypted_data, expected_decrypted_data, "Decrypted data should match the expected value"); + + // Delete temporary files + fs::remove_file(data_file_path).expect("Failed to delete data file"); + fs::remove_file(encrypted_file_path).expect("Failed to delete encrypted file"); + } } \ No newline at end of file -- Gitee From ea9418e65841cd160647787ce659b2e89fdb4b14 Mon Sep 17 00:00:00 2001 From: Kurisu Date: Tue, 30 Jul 2024 15:13:01 +0800 Subject: [PATCH 26/26] feat: set vault file permission to 600. --- src/config/app_vault.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/config/app_vault.rs b/src/config/app_vault.rs index 8c582de..7491c17 100644 --- a/src/config/app_vault.rs +++ b/src/config/app_vault.rs @@ -3,6 +3,8 @@ use anyhow::Context; use serde::{Deserialize, Serialize}; use sha2::Digest; use std::fs; +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use hmac::{Hmac, Mac}; use sha2::Sha256; @@ -49,6 +51,12 @@ impl Vault { // Write the encrypted data to the file fs::write(&file_path, encrypt_data) .context(format!("Failed to write encrypted data to file at {:?}", file_path))?; + + // Set file permissions to 0600 + // Only owner have permissions to read and write + let permissions = Permissions::from_mode(0o600); + fs::set_permissions(&file_path, permissions) + .context(format!("Failed to set permissions for file at {:?}", file_path))?; Ok(()) } -- Gitee