From 67b8a9995e502905e225165fea61519d5521e683 Mon Sep 17 00:00:00 2001 From: Jeremy Massel Date: Fri, 27 Nov 2020 16:29:29 -0700 Subject: [PATCH] =?UTF-8?q?Let=E2=80=99s=20get=20started?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + Cargo.lock | 1102 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 37 ++ README.md | 11 + src/bin.rs | 77 ++++ src/configure.rs | 396 ++++++++++++++++ src/encryption.rs | 82 ++++ src/fs.rs | 234 ++++++++++ src/git.rs | 265 +++++++++++ src/lib.rs | 78 ++++ src/ui.rs | 51 +++ 11 files changed, 2337 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/bin.rs create mode 100644 src/configure.rs create mode 100644 src/encryption.rs create mode 100644 src/fs.rs create mode 100644 src/git.rs create mode 100644 src/lib.rs create mode 100644 src/ui.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69276b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +.DS_Store +/.configure-files +/.configure diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cc74a7e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1102 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "bumpalo" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "bytecount" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39a773ba75db12126d8d383f1bdbf7eb92ea47ec27dd0557aff1fedf172764c" + +[[package]] +name = "cargo_metadata" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8de60b887edf6d74370fc8eb177040da4847d971d6234c7b13a6da324ef0caf" +dependencies = [ + "semver", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "cc" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "configure" +version = "0.1.0" +dependencies = [ + "base64 0.13.0", + "chrono", + "console", + "dialoguer", + "dirs 3.0.1", + "git2", + "indicatif", + "log", + "ring", + "serde", + "serde_json", + "simplelog", + "sodiumoxide", + "structopt", + "structopt-flags", + "thiserror", +] + +[[package]] +name = "console" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50aab2529019abfabfa93f1e6c41ef392f91fbf179b347a7e96abb524884a08" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi", + "winapi-util", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "dialoguer" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f807b2943dc90f9747497d9d65d7e92472149be0b88bf4ce1201b4ac979c26" +dependencies = [ + "console", + "lazy_static", + "tempfile", + "zeroize", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if", + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "git2" +version = "0.13.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6f1a0238d7f8f8fd5ee642f4ebac4dbc03e03d1f78fbe7a3ede35dcf7e2224" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indicatif" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7baab56125e25686df467fe470785512329883aab42696d661247aca2a2896e4" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" + +[[package]] +name = "libgit2-sys" +version = "0.12.14+1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f25af58e6495f7caf2919d08f212de550cfa3ed2f5e744988938ea292b9f549" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libsodium-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a685b64f837b339074115f2e7f7b431ac73681d08d75b389db7498b8892b8a58" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df40b13fe7ea1be9b9dffa365a51273816c345fc1811478b57ed7d964fbfc4ce" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b02fc0ff9a9e4b35b3342880f48e896ebf69f2967921fe8646bf5b7125956a" + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] +name = "openssl-sys" +version = "0.9.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pkg-config" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "pulldown-cmark" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef52fac62d0ea7b9b4dc7da092aa64ea7ec3d90af6679422d3d7e0e14b6ee15" +dependencies = [ + "bitflags", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "regex" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "ring" +version = "0.16.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70017ed5c555d79ee3538fc63ca09c70ad8f317dcadc1adc2c496b60c22bb24f" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rust-argon2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" +dependencies = [ + "base64 0.12.3", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", + "serde", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "simplelog" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf9a002ccce717d066b3ccdb8a28829436249867229291e91b25d99bd723f0d" +dependencies = [ + "chrono", + "log", + "term", +] + +[[package]] +name = "skeptic" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6deb8efaf3ad8fd784139db8bbd51806bfbcee87c7be7578e9c930981fb808" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] +name = "sodiumoxide" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7038b67c941e23501573cb7242ffb08709abe9b11eb74bceff875bbda024a6a8" +dependencies = [ + "libc", + "libsodium-sys", + "serde", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126d630294ec449fae0b16f964e35bf3c74f940da9dca17ee9b905f7b3112eb8" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "structopt-flags" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4654ef901a3897697bc76c48c1d0e73f925e5d801959db6d870d39a87beeae85" +dependencies = [ + "log", + "skeptic", + "structopt", +] + +[[package]] +name = "syn" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443b4178719c5a851e1bde36ce12da21d74a0e60b4d982ec3385a933c812f0f6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "term" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" +dependencies = [ + "dirs 2.0.2", + "winapi", +] + +[[package]] +name = "terminal_size" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd2d183bd3fac5f5fe38ddbeb4dc9aec4a39a9d7d59e7491d900302da01cbe1" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf8dbc19eb42fba10e8feaaec282fb50e2c14b2726d6301dbfeed0f73306a6f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8716a166f290ff49dabc18b44aa407cb7c6dbe1aa0971b44b8a24b0ca35aae" + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" + +[[package]] +name = "web-sys" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[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 = "zeroize" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45af6a010d13e4cf5b54c94ba5a2b2eba5596b9e46bf5875612d332a1f2b3f86" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5c280e8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "configure" +version = "0.1.0" +authors = ["Jeremy Massel "] +edition = "2018" + +[lib] +name = "configure" +path = "src/lib.rs" + +[[bin]] +name = "configure" +path = "src/bin.rs" + +[profile.release] +opt-level = "s" +lto = true +codegen-units = 1 + +[dependencies] +log = "0.4.0" +dirs = "3.0.1" +simplelog = "^0.7.6" +sodiumoxide = "0.2.6" +structopt = { version = "0.3", default-features = false } +structopt-flags = "0.3" +git2 = "0.13" +console = "0.13.0" +dialoguer = "0.7.1" +indicatif = "0.15.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +ring = "0.16.18" +base64 = "0.13.0" + +chrono = "0.4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..202d713 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Configure + +A tool for storing encrypted secrets in your repository, and decrypting them in CI. It allows you to store your configuration files in your source repository, even if that repository is public. + +## How to use it + +The configure tool has two main jobs: copy plain-text secrets files from your secrets repository into the project as encrypted blobs, and decrypting those blogs back into the plain-text files on developer and build machines. + +### Setup + +`configure setup` will walk you through the process of setting up your project. \ No newline at end of file diff --git a/src/bin.rs b/src/bin.rs new file mode 100644 index 0000000..67b680e --- /dev/null +++ b/src/bin.rs @@ -0,0 +1,77 @@ +use log::{debug, LevelFilter}; +use simplelog::CombinedLogger; +use simplelog::Config; +use simplelog::TermLogger; +use simplelog::TerminalMode; +use structopt::StructOpt; +use structopt_flags::GetWithDefault; + +#[derive(StructOpt)] +#[structopt( + name = "configure", + about = "A command-line utility for applying configuration secrets with strong encryption" +)] +struct Options { + #[structopt(subcommand)] + command: Command, + + #[structopt(flatten)] + verbose: structopt_flags::VerboseNoDef, +} + +#[derive(StructOpt)] +enum Command { + /// Update this project's encrypted mobile secrets to the latest version + /// + /// This command will walk the user through updating a project's secrets by: + /// 1. Ensuring that the mobile secrets repository has all the latest data from the server + /// 2. Checking if the user wants to change which mobile secrets branch being used to fetch secrets + /// 3. Prompting the user to update to the latest secrets + /// 4. + + //switch the mobile secrets repo to the pinned commit hash + /// in the `.configure` file, then copy the files specified in the `files_to_copy` hash + /// to their specified destination, encrypting them with the format $filename+".enc". + + /// This command will download the latest mobile secrets commits from the repo + /// and update the pinned commit hash in the `.configure` file to the newest commit + /// in the branch specified by `.configure`. + Update, + + /// Decrypt the current mobile secrets for this project. + /// + Apply, + + /// Change mobile secrets settings + /// + /// This command will provide step-by-step help to make changes to the mobile secrets configuration. + Init, + + /// Ensure the `.configure` file is valid + Validate, + + /// Create a new encryption key for use with a project + CreateKey, +} + +pub fn main() { + let options = Options::from_args(); + + CombinedLogger::init(vec![TermLogger::new( + options.verbose.get_with_default(LevelFilter::Info), + Config::default(), + TerminalMode::Mixed, + ) + .unwrap()]) + .unwrap(); + + debug!("libconfigure initialized"); + + match Options::from_args().command { + Command::Apply => configure::apply(), + Command::Update => configure::update(), + Command::Init => configure::init(), + Command::Validate => configure::validate(), + Command::CreateKey => println!("{:?}", configure::generate_encryption_key()), + } +} diff --git a/src/configure.rs b/src/configure.rs new file mode 100644 index 0000000..c0d3d8c --- /dev/null +++ b/src/configure.rs @@ -0,0 +1,396 @@ +use crate::fs::*; +use crate::git::*; +use crate::ui::*; +use indicatif::ProgressBar; +use chrono::prelude::*; + +use console::style; +use log::{debug, info}; +use serde::{Deserialize, Serialize}; + +use thiserror::Error; + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct ConfigurationFile { + pub project_name: String, + pub branch: String, + pub pinned_hash: String, + pub files_to_copy: Vec, +} + +impl ConfigurationFile { + pub fn is_empty(&self) -> bool { + self == &ConfigurationFile::default() + } + + pub fn get_encryption_key(&self) -> String { + let keys_file_path = crate::fs::find_keys_file().unwrap(); + + debug!("Reading keys from {:?}", keys_file_path); + + let file = std::fs::File::open(keys_file_path).expect("keys file is not readable"); + + let json: serde_json::Value = + serde_json::from_reader(file).expect("keys.json should be valid JSON"); + + let encryption_key = json + .get(&self.project_name) + .expect("keys.json does not contain an encryption key for this project") + .as_str() + .expect("The encryption key for this project is invalid"); + + String::from(encryption_key) + } + + fn needs_project_name(&self) -> bool { + self.project_name == "" + } + + fn needs_branch(&self) -> bool { + self.branch == "" + } + + fn needs_pinned_hash(&self) -> bool { + self.pinned_hash == "" + } +} + +impl Default for ConfigurationFile { + fn default() -> Self { + let files_to_copy: Vec = Vec::new(); + ConfigurationFile { + project_name: "".to_string(), + branch: "".to_string(), + pinned_hash: "".to_string(), + files_to_copy, + } + } +} + +#[derive(Error, Debug)] +pub enum ConfigureError { + #[error("Unable to decrypt file")] + DataDecryptionError(#[from] std::io::Error), + + #[error("Invalid git status")] + GitStatusParsingError(#[from] std::num::ParseIntError), + + #[error("Invalid git status")] + GitStatusUnknownError, + + #[error("No secrets repository could be found on this machine")] + SecretsNotPresent, + + #[error("An encrypted file is missing – unable to apply secrets to project. Run `configure update` to fix this")] + EncryptedFileMissing, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)] +pub struct File { + #[serde(rename = "file")] + pub source: String, + pub destination: String, +} + +impl File { + pub fn get_encrypted_destination(&self) -> String { + self.destination.clone() + &".enc".to_owned() + } + + pub fn get_decrypted_destination(&self) -> String { + self.destination.clone() + } + + pub fn get_backup_destination(&self) -> String { + let path = std::path::Path::new(&self.destination); + + let directory = match path.parent() { + Some(parent) => parent, + None => std::path::Path::new("/"), + }; + + + let file_stem = path.file_stem().unwrap().to_str().unwrap_or(""); + let datetime = Local::now().format("%Y-%m-%d-%H-%M-%S").to_string(); + let extension = path.extension().unwrap_or(std::ffi::OsStr::new("")).to_str().unwrap_or(""); + + let filename = format!("{:}-{:}.{:}.bak", file_stem, datetime, extension); + + return directory + .join(filename) + .to_str() + .unwrap() + .to_string(); + + } +} + +pub fn apply_configuration(configuration: ConfigurationFile) { + // Decrypt the project's configuration files + decrypt_files_for_configuration(&configuration).expect("Unable to decrypt and copy files"); + + debug!("All Files Copied!"); + + info!("Done") +} + +pub fn update_configuration(mut configuration: ConfigurationFile) { + let starting_branch = + get_current_secrets_branch().expect("Unable to determine current mobile secrets branch"); + let starting_ref = + get_secrets_current_hash().expect("Unable to determine current mobile secrets commit hash"); + + heading("Configure Update"); + + // + // Step 1 – Fetch the latest mobile secrets from the server + // We need them in order to update the pinned hash + // + let bar = ProgressBar::new_spinner(); + bar.enable_steady_tick(125); + bar.set_message("Fetching Latest Mobile Secrets"); + + fetch_secrets_latest_remote_data().expect("Unable to fetch latest mobile secrets"); + + bar.finish_and_clear(); + + // + // Step 2 – Check if the user wants to use a different secrets branch + // + configuration = prompt_for_branch(configuration, true); + + // + // Step 3 – Check if the currente configuration branch is in sync with the server or not.or + // If not, check with the user whether they'd like to continue + // + let status = get_secrets_repo_status().expect("Unable to get secrets repo status"); + + let should_continue = match status.sync_state { + RepoSyncState::Ahead => { + warn(&format!( + "Your local secrets repo has {:?} change(s) that the server does not", + status.distance + )); + confirm("Would you like to continue?") + } + RepoSyncState::Behind => { + warn(&format!( + "The server has {:?} change(s) that your local secrets repo does not", + status.distance + )); + confirm("Would you like to continue?") + } + RepoSyncState::Synced => true, + }; + + if !should_continue { + return; + } + + // + // Step 4 – Check if the project's secrets are out of date compared to the server. + // If they out of date, we'll prompt the user to pull the latest remote + // changes into the local secrets repo before continuing. + // + let distance = + configure_file_distance_behind_secrets_repo(&configuration, &configuration.branch); + if distance > 0 { + let message = format!( + "This project is {:?} commit(s) behind the latest secrets. Would you like to use the latest secrets?", + distance + ); + + // Prompt to update to most recent secrets data in the branch + if confirm(&message) { + let latest_commit_hash = get_latest_hash_for_remote_branch(&configuration.branch) + .expect("Unable to fetch latest commit hash"); + + debug!( + "Moving the repo to {:?} at {:?}", + &configuration.branch, latest_commit_hash + ); + + check_out_branch_at_revision(&configuration.branch, &latest_commit_hash) + .expect("Unable to check out branch at revision"); + configuration.pinned_hash = latest_commit_hash; + } + } + + // + // Step 5 – Write out encrypted files as needed + // + save_configuration(&configuration).expect("Unable to save updated configuration"); + + // + // Step 6 – Write out encrypted files as needed + // + write_encrypted_files_for_configuration(&configuration) + .expect("Unable to copy encrypted files"); + + // + // Step 7 – Roll everything back to how it was before we started + // + crate::git::check_out_branch_at_revision(&starting_branch, &starting_ref) + .expect("Unable to roll back to branch"); + + // + // Step 8 – Apply these changes to the current repo + // + apply_configuration(configuration); +} + +pub fn validate_configuration(configuration: ConfigurationFile) { + println!("{:?}", configuration); +} + +pub fn setup_configuration(mut configuration: ConfigurationFile) { + heading("Configure Setup"); + println!("Let's get configuration set up for this project."); + newline(); + + // Help the user set the `project_name` field + configuration = prompt_for_project_name_if_needed(configuration); + + // Help the user set the `branch` field + configuration = prompt_for_branch(configuration, true); + + // Set the latest automatically hash based on the selected branch + configuration = set_latest_hash_if_needed(configuration); + + // Help the user add files + configuration = prompt_to_add_files(configuration); + + info!("Writing changes to .configure"); + + save_configuration(&configuration).expect("Unable to save configure file"); +} + +fn prompt_for_project_name_if_needed(mut configuration: ConfigurationFile) -> ConfigurationFile { + // If there's already a project name, don't bother updating it + if !configuration.needs_project_name() { + return configuration; + } + + let project_name = prompt("What is the name of your project?"); + configuration.project_name = project_name.clone(); + println!("Project Name set to: {:?}", project_name); + + configuration +} + +fn prompt_for_branch(mut configuration: ConfigurationFile, force: bool) -> ConfigurationFile { + // If there's already a branch set, don't bother updating it + if !configuration.needs_branch() && !force { + return configuration; + } + + let secrets_repo_path = find_secrets_repo(); + let current_branch = + get_current_secrets_branch().expect("Unable to determine current mobile secrets branch"); + let branches = get_secrets_branches().expect("Unable to fetch mobile secrets branches"); + + println!( + "We've found your mobile secrets repository at {:?}", + secrets_repo_path + ); + newline(); + println!("Which branch would you like to use?"); + println!("Current Branch: {}", style(¤t_branch).green()); + + let selected_branch = + select(branches, ¤t_branch).expect("Unable to read selected branch"); + + configuration.branch = selected_branch.clone(); + println!("Secrets repo branch set to: {:?}", selected_branch); + + configuration +} + +fn set_latest_hash_if_needed(mut configuration: ConfigurationFile) -> ConfigurationFile { + if !configuration.needs_pinned_hash() { + return configuration; + } + + let latest_hash = get_secrets_latest_hash(&configuration.branch) + .expect("Unable to fetch the latest secrets hash"); + configuration.pinned_hash = latest_hash; + + configuration +} + +fn prompt_to_add_files(mut configuration: ConfigurationFile) -> ConfigurationFile { + let mut files = configuration.files_to_copy; + + let mut message = "Would you like to add files?"; + + if !files.is_empty() { + message = "Would you like to add additional files?"; + } + + while confirm(message) { + match prompt_to_add_file() { + Some(file) => files.push(file), + None => continue, + } + } + + configuration.files_to_copy = files; + + configuration +} + +fn prompt_to_add_file() -> Option { + let relative_source_file_path = + prompt("Enter the source file path (relative to the secrets root):"); + + let secrets_root = match find_secrets_repo() { + Ok(repo_path) => repo_path, + Err(_) => return None, + }; + + let full_source_file_path = secrets_root.join(&relative_source_file_path); + + if !full_source_file_path.exists() { + println!("Source File does not exist: {:?}", full_source_file_path); + return None; + } + + let relative_destination_file_path = + prompt("Enter the destination file path (relative to the project root):"); + + let project_root = find_project_root(); + let full_destination_file_path = project_root.join(&relative_destination_file_path); + + debug!("Destination: {:?}", full_destination_file_path); + + Some(File { + source: relative_source_file_path, + destination: relative_destination_file_path, + }) +} + +fn configure_file_distance_behind_secrets_repo( + configuration: &ConfigurationFile, + branch_name: &str, +) -> i32 { + debug!("Checking if configure file is behind secrets repo"); + + let current_branch = + get_current_secrets_branch().expect("Unable to get current mobile secrets branch"); + debug!("Current branch is: {:?}", current_branch); + + let current_hash = + get_secrets_current_hash().expect("Unable to get current mobile secrets hash"); + debug!("Current hash is: {:?}", current_hash); + + check_out_branch(branch_name).expect("Unable to switch branches"); + + let latest_hash = get_secrets_current_hash().unwrap(); + let distance = secrets_repo_distance_between(&configuration.pinned_hash, &latest_hash).unwrap(); + + // Put things back how we found them + crate::git::check_out_branch_at_revision(¤t_branch, ¤t_hash) + .expect("Unable to roll back to branch"); + + distance +} diff --git a/src/encryption.rs b/src/encryption.rs new file mode 100644 index 0000000..e6b8b51 --- /dev/null +++ b/src/encryption.rs @@ -0,0 +1,82 @@ +use log::debug; +use sodiumoxide::base64::Variant; +use sodiumoxide::base64::{decode, encode}; +use sodiumoxide::crypto::secretbox; +use std::fs::{read, write}; +use std::io::{Error, ErrorKind}; +use std::path::PathBuf; + +pub fn init() { + sodiumoxide::init().expect("Unable to initialize libsodium"); +} + +pub fn generate_key() -> String { + debug!("Generating an encryption key"); + let key_bytes = secretbox::gen_key(); + encode_key(key_bytes) +} + +pub fn encrypt_file( + input_path: &PathBuf, + output_path: &PathBuf, + secret: &str, +) -> Result<(), std::io::Error> { + let content = read(input_path)?; + let ciphertext = encrypt_bytes(content, decode_key(secret)); + write(&output_path, &ciphertext)?; + + Ok(()) +} + +pub fn decrypt_file( + input_path: &PathBuf, + output_path: &PathBuf, + secret: &str, +) -> Result<(), std::io::Error> { + let content = read(input_path)?; + + match decrypt_bytes(content, decode_key(secret)) { + Ok(decrypted_bytes) => Ok(write(&output_path, decrypted_bytes)?), + Err(_err) => Err(Error::new(ErrorKind::InvalidData, "Unable to decrypt file")), + } +} + +fn encrypt_bytes(input: Vec, key: sodiumoxide::crypto::secretbox::Key) -> Vec { + let nonce = secretbox::gen_nonce(); + let secret_bytes = secretbox::seal(&input, &nonce, &key); + [&nonce[..], &secret_bytes].concat() +} + +fn decrypt_bytes(input: Vec, key: sodiumoxide::crypto::secretbox::Key) -> Result, ()> { + // Encoded Format byte layout: + // |======================================|=====================================| + // | 0 23 | 24 ∞ | + // |======================================|=====================================| + // | nonce | encrypted data | + // |======================================|=====================================| + + const NONCE_SIZE: usize = 24; + + // Read the nonce bytes + let mut nonce_bytes: [u8; NONCE_SIZE] = Default::default(); + nonce_bytes.copy_from_slice(&input[0..NONCE_SIZE]); + let nonce = sodiumoxide::crypto::secretbox::Nonce(nonce_bytes); + + // Read the encrypted data bytes + let data_bytes = &input[NONCE_SIZE..]; + + Ok(secretbox::open(&data_bytes, &nonce, &key)?) +} + +fn encode_key(key: sodiumoxide::crypto::secretbox::Key) -> String { + encode(&key, Variant::Original) +} + +fn decode_key(key: &str) -> sodiumoxide::crypto::secretbox::Key { + let decoded_key_bytes = decode(key, Variant::Original).expect("Unable to decode key"); + + let mut key_bytes: [u8; 32] = Default::default(); + key_bytes.copy_from_slice(&decoded_key_bytes); + + sodiumoxide::crypto::secretbox::Key(key_bytes) +} diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..8a29d86 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,234 @@ +use crate::encryption::{decrypt_file, encrypt_file}; +use crate::ConfigurationFile; +use crate::ConfigureError; +use log::{debug, info}; +use ring::digest::{Context, SHA256}; +use std::env; +use std::fs::{create_dir_all, remove_file, rename, File}; +use std::io::{BufReader, Error, Read, Write}; +use std::path::PathBuf; + +/// Find the .configure file in the current project +pub fn find_configure_file() -> PathBuf { + let project_root = find_project_root(); + + let configure_file_path = project_root.join(".configure"); + + debug!("Configure file found at: {:?}", configure_file_path); + + if !configure_file_path.exists() { + info!( + "No configure file found at: {:?}. Creating one for you", + configure_file_path + ); + + save_configuration(&ConfigurationFile::default()) + .expect("There is no `configure.json` file in your project, and creating one failed"); + } + + configure_file_path +} + +pub fn find_keys_file() -> Result { + let secrets_root = find_secrets_repo(); + let keys_file_path = secrets_root?.join("keys.json"); + + debug!("Keys file found at: {:?}", keys_file_path); + + if !keys_file_path.exists() { + info!( + "No keys file found at: {:?}. Creating one for you", + keys_file_path + ); + create_file_with_contents(&keys_file_path, "{}").expect( + "There is no `keys.json` file in your secrets repository, and creating one failed", + ); + } + + Ok(keys_file_path) +} + +pub fn find_project_root() -> PathBuf { + let path = env::current_dir().expect("Unable to determine current directory"); + + let repo = git2::Repository::discover(&path) + .expect("Unable to find the root of the respository – are you sure you're running this inside a git repo?"); + + debug!("Discovered Repository at {:?}", &path); + + repo.workdir().unwrap().to_path_buf() +} + +pub fn find_secrets_repo() -> Result { + // TODO: Allow the user to set their own mobile secrets path using an environment variable + + let home_dir = dirs::home_dir().expect("Unable to determine user home directory"); + + let root_secrets_path = home_dir.join(".mobile-secrets"); + + if root_secrets_path.exists() && root_secrets_path.is_dir() { + return Ok(root_secrets_path); + } + + // If the user has a `Projects` directory + let projects_path = home_dir.join("Projects"); + if projects_path.exists() { + let projects_secrets_path = projects_path.join(".mobile-secrets"); + if projects_secrets_path.exists() && projects_secrets_path.is_dir() { + return Ok(projects_secrets_path); + } + } + + Err(crate::configure::ConfigureError::SecretsNotPresent) +} + +pub fn read_configuration() -> ConfigurationFile { + let configure_file_path = find_configure_file(); + let mut file = File::open(&configure_file_path).expect("Unable to open configuration file"); + + let mut file_contents = String::new(); + file.read_to_string(&mut file_contents) + .expect("Unable to read configuration file"); + + let result: ConfigurationFile = serde_json::from_str(&file_contents) + .expect("Unable to parse configuration file – the JSON is probably invalid"); + + result +} + +pub fn save_configuration(configuration: &ConfigurationFile) -> Result<(), Error> { + let serialized = serde_json::to_string_pretty(&configuration)?; + + let configure_file = find_configure_file(); + + debug!("Writing to: {:?}", configure_file); + + let mut file = File::create(configure_file)?; + file.write_all(serialized.as_bytes())?; + Ok(()) +} + +pub fn decrypt_files_for_configuration( + configuration: &ConfigurationFile, +) -> Result<(), ConfigureError> { + let project_root = find_project_root(); + let encryption_key = configuration.get_encryption_key(); + + for file in &configuration.files_to_copy { + let source = project_root.join(&file.get_encrypted_destination()); + let destination = project_root.join(&file.get_decrypted_destination()); + + create_parent_directory_for_path_if_not_exists(&destination)?; + + // If the developer tries to run `configure_apply` while missing the encrypted originals, this script will crash saying "missing file" + // We can try to detect this scenario and fix things for the developer if the mobile secrets are available locally, but it's tricky because + // we'd need to basically run `configure update` inside this method for just the one file. For now, we'll just error out. + if !source.exists() { + info!("Encrypted original file at {:?} not found", source); + return Err(ConfigureError::EncryptedFileMissing {}); + } + + // If the file already exists, make a backup of the old one in case we need it later + if destination.exists() { + let backup_destination = project_root.join(&file.get_backup_destination()); + + debug!( + "{:?} already exists – making a backup at {:?}", + destination, backup_destination + ); + rename(&destination, &backup_destination)?; + + // Encrypt the file and write the encrypted contents to the destination + debug!( + "Encrypting file at {:?} and storing contents at {:?}", + source, destination + ); + decrypt_file(&source, &destination, &encryption_key)?; + + // If the backup file is identical to the old file, remove the backup + let new_file_hash = hash_file(&destination); + let original_file_hash = hash_file(&backup_destination); + + debug!("Original File Hash: {:?}", original_file_hash); + debug!("New File hash: {:?}", new_file_hash); + + if hash_file(&destination)? == hash_file(&backup_destination)? { + debug!("Removing backup file because it's the same as the original"); + remove_file(&backup_destination)?; + } else { + debug!("Keeping backup file because it differs from the original"); + } + + } else { + // Encrypt the file and write the encrypted contents to the destination + debug!( + "Encrypting file at {:?} and storing contents at {:?}", + source, destination + ); + decrypt_file(&source, &destination, &encryption_key)?; + } + } + + Ok(()) +} + +pub fn write_encrypted_files_for_configuration( + configuration: &ConfigurationFile, +) -> Result<(), Error> { + let project_root = find_project_root(); + let secrets_root = find_secrets_repo().unwrap(); + let encryption_key = configuration.get_encryption_key(); + + for file in &configuration.files_to_copy { + let source = &secrets_root.join(&file.source); + let destination = project_root.join(&file.get_encrypted_destination()); + + create_parent_directory_for_path_if_not_exists(&destination)?; + + // Encrypt the file and write the encrypted contents to the destination + debug!( + "Encrypting file at {:?} and storing contents at {:?}", + source, destination + ); + + encrypt_file(&source, &destination, &encryption_key)?; + } + + Ok(()) +} + +/// Helper method to create an empty file +fn create_file_with_contents(path: &PathBuf, contents: &str) -> Result<(), std::io::Error> { + let mut file = File::create(path)?; + file.write_all(contents.as_bytes())?; + Ok(()) +} + +/// Returns the SHA-256 hash of a file at the given path +fn hash_file(path: &PathBuf) -> Result { + let input = File::open(path)?; + let mut reader = BufReader::new(input); + let mut context = Context::new(&SHA256); + let mut buffer = [0; 1024]; + + loop { + let count = reader.read(&mut buffer)?; + if count == 0 { + break; + } + context.update(&buffer[..count]); + } + + let digest = context.finish(); + + Ok(base64::encode(digest.as_ref())) +} + +fn create_parent_directory_for_path_if_not_exists(path: &PathBuf) -> Result<(), Error> { + let parent = match path.parent() { + Some(parent) => parent, + None => return Ok(()), // if we're in the root of the filesystem, we have no work to do + }; + + Ok(create_dir_all(parent)?) +} diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..e3b7536 --- /dev/null +++ b/src/git.rs @@ -0,0 +1,265 @@ +use crate::ConfigureError; +use git2::Oid; +use git2::{BranchType, Error, ErrorCode, Repository, ResetType}; +use log::debug; + +pub fn get_current_secrets_branch() -> Result { + let repo = get_secrets_repo()?; + let head = match repo.head() { + Ok(head) => Some(head), + Err(ref e) if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound => { + None + } + Err(e) => return Err(e), + }; + + let head = head.as_ref().and_then(|h| h.shorthand()); + + Ok(head.unwrap().to_string()) +} + +pub fn get_secrets_branches() -> Result, Error> { + let repo = get_secrets_repo()?; + let branches = repo.branches(Some(BranchType::Local))?; + let branch_names: Vec = branches + .into_iter() + .map(|branch| branch.expect("Unable to read branch")) + .map(|branch| { + String::from( + branch + .0 + .name() + .expect("Unable to read branch name") + .unwrap(), + ) + }) + .collect::>(); + + Ok(branch_names) +} + +// Assumes you're using `origin` as the remote name +pub fn fetch_secrets_latest_remote_data() -> Result<(), std::io::Error> { + let path = crate::fs::find_secrets_repo().unwrap(); + + std::process::Command::new("git") + .arg("fetch") + .current_dir(std::fs::canonicalize(path).unwrap()) + .output()?; // Wait for it to finish and collect its output + + debug!("Fetch Complete"); + + Ok(()) +} + +pub fn get_secrets_current_hash() -> Result { + let repo = get_secrets_repo()?; + let latest_commit = repo.head()?.peel_to_commit()?; + Ok(latest_commit.id().to_string()) +} + +// Fetches the latest hash on the specified branch +// +// You should run `fetch_secrets_latest_remote_data` before this method, otherwise your info might be out-of-date +pub fn get_secrets_latest_hash(branch: &str) -> Result { + if get_current_secrets_branch().unwrap() != branch {} + + let repo = get_secrets_repo()?; + let latest_commit = repo.head()?.peel_to_commit()?; + + Ok(latest_commit.id().to_string()) +} + +pub fn get_latest_hash_for_remote_branch(branch: &str) -> Result { + let path = crate::fs::find_secrets_repo().unwrap(); + + let remote_ref = "origin/".to_owned() + branch; + + debug!("Looking for remote ref: {:?}", remote_ref); + + let output = std::process::Command::new("git") + .arg("rev-parse") + .arg(remote_ref) + .current_dir(std::fs::canonicalize(path).unwrap()) + .output()?; // Wait for it to finish and collect its output + + let string = std::str::from_utf8(&output.stdout).expect("Unable to parse output"); + + debug!("Result: {}", string); + + Ok(String::from(string.trim_end())) +} + +pub fn check_out_hash(hash: &str) -> Result<(), Error> { + let repo = get_secrets_repo()?; + + let oid = Oid::from_str(hash).expect("Invalid Hash"); + + let obj = repo + .find_commit(oid) + .expect("No commit exists with that hash") + .into_object(); + + repo.set_head_detached(oid)?; + repo.reset(&obj, ResetType::Hard, None)?; + + Ok(()) +} + +pub fn check_out_branch(branch_name: &str) -> Result<(), Error> { + debug!("Trying to check out branch: {:?}", branch_name); + let repo = get_secrets_repo()?; + let ref_name = "refs/heads/".to_owned() + branch_name; + debug!("Checking out: {:?}", ref_name); + + repo.set_head(&ref_name)?; + + debug!("Checkout successful"); + Ok(()) +} + +pub fn check_out_branch_at_revision(branch_name: &str, hash: &str) -> Result<(), Error> { + // If we're asked to check out a commit that's not currently on a branch, + // just switch to it directly + if branch_name == "HEAD" { + return check_out_hash(hash); + } + + let repo = get_secrets_repo()?; + let ref_name = "refs/heads/".to_owned() + branch_name; + + repo.set_head(&ref_name)?; + + let oid = Oid::from_str(hash).expect("Invalid Hash"); + + let obj = repo + .find_commit(oid) + .expect("No commit exists with that hash") + .into_object(); + + repo.reset(&obj, ResetType::Hard, None)?; + + Ok(()) +} + +// Returns the number of commits between two hashes. If the hashes aren't part of the same history +// or if `hash2` comes before `hash1`, the result will be `0` +pub fn secrets_repo_distance_between(hash1: &str, hash2: &str) -> Result { + // If we're asked to calculate the distance between two of the same hash, we can skip a lot of work + if hash1 == hash2 { + return Ok(0); + } + + let path = crate::fs::find_secrets_repo().unwrap(); + + let output = std::process::Command::new("git") + .arg("--no-pager") + .arg("log") + .arg("-10000") + .arg("--pretty=format:%H") + .current_dir(std::fs::canonicalize(path).unwrap()) + .output()?; + + let iter = std::str::from_utf8(&output.stdout) + .expect("Unable to read hash list") + .lines() + .rev(); + + let index_of_configure_file_hash = iter + .clone() + .position(|r| r == "7a44543420761867bfb80f95c8864702d41059e3") + .unwrap_or_else(|| { + panic!( + "The pinned hash in .configure {} doesn't exist in the repository history", + &hash1 + ) + }); + + let index_of_latest_repo_hash = iter.clone().position(|r| r == hash2).unwrap_or_else(|| { + panic!( + "The provided hash {} doesn't exist in the repository history", + &hash2 + ) + }); + + let distance = std::cmp::min(index_of_latest_repo_hash - index_of_configure_file_hash, 0); + + Ok(distance as i32) +} + +pub enum RepoSyncState { + /// The local secrets repository has commits that the server does not have + Ahead, + + /// The server has commits that the local secrets repository does not have + Behind, + + /// The local secrets repository and server are in sync + Synced, +} + +pub struct RepoStatus { + /// The local repository sync state – ahead of, behind, or in sync with the server + pub sync_state: RepoSyncState, + + /// How many commits the local repository is out of sync by. If the repository is in sync, + /// this value will be `0` + pub distance: i32, +} + +impl RepoStatus { + fn synced() -> RepoStatus { + RepoStatus { + sync_state: RepoSyncState::Synced, + distance: 0, + } + } +} + +pub fn get_secrets_repo_status() -> Result { + let path = crate::fs::find_secrets_repo()?; + + let output = std::process::Command::new("git") + .arg("status") + .arg("--porcelain") + .arg("-b") + .current_dir(std::fs::canonicalize(path).unwrap()) + .output()?; // Wait for it to finish and collect its output + + let status = std::str::from_utf8(&output.stdout).expect("Unable to read output data"); + + Ok(parse_repo_status(status)?) +} + +fn parse_repo_status(status: &str) -> Result { + if status.contains("...") { + return Ok(RepoStatus::synced()); + } + + let digits = status + .chars() + .filter(|c| c.is_digit(10)) + .collect::() + .parse::()?; + + if status.contains("ahead") { + return Ok(RepoStatus { + sync_state: RepoSyncState::Ahead, + distance: digits, + }); + } + + if status.contains("behind") { + return Ok(RepoStatus { + sync_state: RepoSyncState::Behind, + distance: digits, + }); + } + + Err(ConfigureError::GitStatusUnknownError {}) +} + +fn get_secrets_repo() -> Result { + let path = crate::fs::find_secrets_repo().unwrap(); + Ok(Repository::open(path)?) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b668ba6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,78 @@ +mod configure; +mod encryption; +mod fs; +mod git; +mod ui; + +use crate::configure::*; +use crate::fs::*; +use log::debug; + +/// Set up a project to use the configure tool +/// +pub fn init() { + init_encryption(); + let configuration = read_configuration(); + setup_configuration(configuration); +} + +/// Decrypts secrets already present in the repository +/// +/// To get secrets into the repository, use `configure_update` +/// +/// # Arguments +/// +/// * `configuration` - The project's parsed `ConfigurationFile` object. +/// +pub fn apply() { + init_encryption(); + let configuration = read_configuration(); + + if !configuration.is_empty() { + apply_configuration(configuration); + } else { + setup_configuration(configuration); + } +} + +/// Adds encrypted secrets files to the configuration, or updates existing ones. +/// +/// Prompts the user to decrypt them when it finishes. +/// +/// # Arguments +/// +/// * `configuration` - The project's parsed `ConfigurationFile` object. +/// +pub fn update() { + init_encryption(); + let configuration = read_configuration(); + + if !configuration.is_empty() { + update_configuration(configuration); + } else { + setup_configuration(configuration); + } +} + +/// Validate a project's .configure file +/// +pub fn validate() { + init_encryption(); + let configuration = read_configuration(); + + if !configuration.is_empty() { + validate_configuration(configuration); + } else { + setup_configuration(configuration); + } +} + +pub fn generate_encryption_key() -> String { + crate::encryption::generate_key() +} + +fn init_encryption() { + debug!("libConfigure initializing encryption"); + encryption::init(); + debug!("libConfigure encryption initialization successful"); +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..115b684 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,51 @@ +use console::{style, Term}; +use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; + +/// +/// Print a heading-style message to the console +pub fn heading(string: &str) { + println!("{}", style(string).green()); +} + +/// +/// Print a warning to the console +pub fn warn(string: &str) { + println!("{}", style(string).yellow()); +} + +/// +/// Print a blank line to the console +pub fn newline() { + println!(); +} + +/// +/// Prompt the user to input text on the command line +pub fn prompt(message: &str) -> String { + heading(message); + Input::::new().interact_text().unwrap() +} + +/// +/// Ask the user for confirmation +pub fn confirm(message: &str) -> bool { + Confirm::new().with_prompt(message).interact().unwrap() +} + +/// +/// Allow the user to provide a list of items to select from +pub fn select(items: Vec, selected: &str) -> Result { + let index_of_current_branch = items + .iter() + .position(|name| *name == selected) + .expect("Unable to find current branch in repo branch list"); + + let selection = Select::with_theme(&ColorfulTheme::default()) + .items(&items) + .default(index_of_current_branch) + .interact_on_opt(&Term::stderr()) + .expect("You must select an option") + .unwrap(); + + Ok(items[selection].clone()) +}