commit 8a8383cee3ace3750fce7128777c395f722f1a33 Author: German Lashevich Date: Mon Dec 25 00:09:35 2017 +0100 Initial version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87ee2d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target/ +**/*.rs.bk +.history diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..29b5a5c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,380 @@ +[[package]] +name = "ansi_term" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "atty" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", + "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "base32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byte-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "clap" +version = "2.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "crypto-mac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "digest" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "digest-buffer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byte-tools 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "dtoa" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fuchsia-zircon" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-zircon-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "generic-array" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hmac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "crypto-mac 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "nodrop" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "num-traits" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "oath" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "digest 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "hmac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "sha-1 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rand" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-zircon 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "redox_termios" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rustc-hex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustotpony" +version = "0.1.0" +dependencies = [ + "base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)", + "oath 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive_internals 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive_internals" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha-1" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byte-tools 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "digest-buffer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byte-tools 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "digest-buffer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termion" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typenum" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-width" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455" +"checksum atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "21e50800ec991574876040fff8ee46b136a53e985286fbe6a3bdfe6421b78860" +"checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c" +"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" +"checksum byte-tools 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0919189ba800c7ffe8778278116b7e0de3905ab81c72abb69c85cbfef7991279" +"checksum clap 2.29.0 (registry+https://github.com/rust-lang/crates.io-index)" = "110d43e343eb29f4f51c1db31beb879d546db27998577e5715270a54bcf41d3f" +"checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e" +"checksum crypto-mac 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dba62c86c26dcba13c278afcaac0c7452486fe604a2668a0dfa4e0edc98d8a9e" +"checksum digest 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7a68d759d7a66a4f63d5bd2a2b14ad7e8cf93fe8c9be227031cd4e72ab0e9ee8" +"checksum digest-buffer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4eb92364e9f6d3da159257250532d448b218406d2acb149f724e8f48e9f5cb9a" +"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +"checksum fuchsia-zircon 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bd510087c325af53ba24f3be8f1c081b0982319adcb8b03cad764512923ccc19" +"checksum fuchsia-zircon-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "08b3a6f13ad6b96572b53ce7af74543132f1a7055ccceb6d073dd36c54481859" +"checksum generic-array 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe043cf9b85297937897087de81f590361686e1ac2d4d471b45435de5dfb6a6" +"checksum hmac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdb5aa9647ba4711e9d6968dc1c810cd23989ed435443ca962e1bf6d8b8b83ff" +"checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)" = "36fbc8a8929c632868295d0178dd8f63fc423fd7537ad0738372bd010b3ac9b0" +"checksum nodrop 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "9a2228dca57108069a5262f2ed8bd2e82496d2e074a06d1ccc7ce1687b6ae0a2" +"checksum num-traits 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "cacfcab5eb48250ee7d0c7896b51a2c5eec99c1feea5f32025635f5ae4b00070" +"checksum oath 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ec6405dc6afe8219020d535f9ad888a12b191bbc8ce1c55f7ee663bde5be80ca" +"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" +"checksum rand 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9d5f78082e6a6d042862611e9640cf20776185fee506cf6cf67e93c6225cee31" +"checksum redox_syscall 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "ab105df655884ede59d45b7070c8a65002d921461ee813a024558ca16030eea0" +"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" +"checksum rustc-hex 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0ceb8ce7a5e520de349e1fa172baeba4a9e8d5ef06c47471863530bc4972ee1e" +"checksum serde 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1c57ab4ec5fa85d08aaf8ed9245899d9bbdd66768945b21113b84d5f595cb6a1" +"checksum serde_derive 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "02c92ea07b6e49b959c1481804ebc9bfd92d3c459f1274c9a9546829e42a66ce" +"checksum serde_derive_internals 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75c6aac7b99801a16db5b40b7bf0d7e4ba16e76fbf231e32a4677f271cac0603" +"checksum serde_json 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7cf5b0b5b4bd22eeecb7e01ac2e1225c7ef5e4272b79ee28a8392a8c8489c839" +"checksum sha-1 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8347606816471548cd60f0abd5ef0d513a81f5202dbdab9c09f17a15b5248484" +"checksum sha2 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "84920f9ac881e94e33ec89e1b3dcd36040523a308a92548e01217ce35d8cf6a8" +"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694" +"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" +"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" +"checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" +"checksum typenum 1.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a99dc6780ef33c78780b826cf9d2a78840b72cae9474de4bcaf9051e60ebbd" +"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" +"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" +"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..189e01d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["German Lashevich "] +name = "rustotpony" +version = "0.1.0" + +[dependencies] +base32 = "0.3.1" +clap = "^2.29.0" +oath = "0.10.2" +rand = "0.4.1" +serde = "1.0.24" +serde_derive = "1.0.24" +serde_json = "1.0.8" diff --git a/README.md b/README.md new file mode 100644 index 0000000..ea4cc75 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# RusTOTPony + +## Usage + +Base commads (simple CRUD): + +- show dashboard +- show all apps + - with codes + - without codes +- show code for app +- add app (title, subtitle, key) +- rename app +- delete app +- clear database + +```s +$ totp dash +$ totp list +$ totp show-all +$ totp show APPNAME +$ totp add APPNAME KEY # ask for key interactively? +$ totp delete APPNAME # with confirmation +$ totp rename APPNAME NEWNAME +$ totp eradicate # with non-trivial confirmation +``` + +## TODO + +- completion +- encription with (manual or with keychain) +- password caching \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5dd686e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,451 @@ +extern crate base32; +extern crate clap; +extern crate oath; +extern crate rand; +extern crate serde_json; + +#[macro_use] +extern crate serde_derive; + +use clap::{App, Arg, SubCommand}; +use std::collections::HashMap; +use std::fs::{create_dir_all, File, OpenOptions}; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::thread; +use std::time::Duration; + + +const CONFIG_PATH: &str = ".rustotpony/db.json"; +const DATABASE_VERSION: u8 = 1; + +struct RusTOTPony { + database: DB, + applications: HashMap, +} + +impl RusTOTPony { + fn new(db: DB) -> RusTOTPony { + RusTOTPony { + applications: db.get_applications(), + database: db, + } + } + + fn add_application(&mut self, name: &str, secret: &str) -> Result<(), String> { + if let Some(secret_bytes) = GenApp::base32_to_bytes(secret) { + let new_app = GenApp::new(name, secret, secret_bytes); + if self.applications.contains_key(name) { + Err(format!("Application with name '{}' already exists!", name)) + } else { + &self.applications.insert(String::from(name), new_app); + Ok(()) + } + } else { + return Err(String::from("Couldn't decode secret key")) + } + } + + fn delete_application(&mut self, name: &str) -> Result<(), String> { + if let Some(_) = self.applications.remove(name) { + Ok(()) + } else { + Err(format!( + "Application with the name '{}' doesn't exist", + name + )) + } + } + + fn rename_application(&mut self, name: &str, newname: &str) -> Result<(), String> { + if let Some(app) = self.applications.get_mut(name) { + app.name = String::from(newname); + Ok(()) + } else { + Err(format!("Application '{}' wasn't found", name)) + } + } + + fn get_applications(&self) -> Result<&HashMap, String> { + if self.applications.len() == 0 { + Err(String::from("There are no applications")) + } else { + Ok(&self.applications) + } + } + + fn get_application(&self, name:&str) -> Result<&GenApp, String> { + if let Some(app) = self.applications.get(name) { + Ok(app) + } else { + Err(format!("Application '{}' wasn't found", name)) + } + } + + fn delete_all_applications(&mut self) { + self.applications = HashMap::new(); + } + + fn flush(&self) { + &self.database.save_applications(&self.applications); + } +} + +trait Database { + fn get_applications(&self) -> HashMap; + fn save_applications(&self, applications: &HashMap); +} + +impl Database for JsonDatabase { + fn get_applications(&self) -> HashMap { + let db_content = self.read_database_file(); + db_content.content.applications + } + + fn save_applications(&self, applications: &HashMap) { + let mut db_content = Self::get_empty_schema(); + db_content.content.applications = applications.clone(); + self.save_database_file(db_content); + } +} + +#[derive(Serialize, Deserialize)] +struct JsonDatabaseSchema { + version: u8, + content: DatabaseContentSchema, +} + +#[derive(Serialize, Deserialize)] +struct DatabaseContentSchema { + applications: HashMap, +} + +struct JsonDatabase { + file_path: PathBuf, +} + +impl JsonDatabase { + fn new(path: PathBuf) -> JsonDatabase { + JsonDatabase { file_path: path } + } + + fn read_database_file(&self) -> JsonDatabaseSchema { + let file = match File::open(&self.file_path) { + Ok(f) => f, + Err(ref err) if err.kind() == ErrorKind::NotFound => return Self::get_empty_schema(), + Err(err) => panic!("There was a problem opening file: {:?}", err), + }; + serde_json::from_reader(file).expect("Couldn't parse JSON from database file") + } + + fn save_database_file(&self, content: JsonDatabaseSchema) { + let file = match self.open_database_file_for_write() { + Ok(f) => f, + Err(ref err) if err.kind() == ErrorKind::NotFound => self.create_database_file() + .expect("Couldn't create database file"), + Err(err) => panic!("Couldn't open database file: {:?}", err), + }; + serde_json::to_writer(file, &content).expect("Couldn't write JSON data to database file"); + } + + fn create_database_file(&self) -> Result { + let dir = std::env::home_dir().unwrap_or(PathBuf::from(".")); + if let Some(parent_dir) = Path::new(&self.file_path).parent() { + let dir = dir.join(parent_dir); + create_dir_all(dir)?; + } + self.open_database_file_for_write() + } + + fn open_database_file_for_write(&self) -> Result { + OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(&self.file_path) + } + + fn get_empty_schema() -> JsonDatabaseSchema { + JsonDatabaseSchema { + version: DATABASE_VERSION, + content: DatabaseContentSchema { + applications: HashMap::new(), + }, + } + } +} + + +#[derive(Serialize, Deserialize, Debug, Clone)] +struct GenApp { + name: String, + secret: String, + username: String, + secret_bytes: Vec, +} + +impl GenApp { + fn new(name: &str, secret: &str, secret_bytes: Vec) -> Self { + GenApp { + name: String::from(name), + secret: String::from(secret), + username: String::from(""), + secret_bytes: secret_bytes, + } + } + + fn get_code(&self) -> u64 { + Self::totp(&self.secret_bytes) + } + + fn base32_to_bytes(secret:&str) -> Option> { + base32::decode(base32::Alphabet::RFC4648 {padding: false}, secret) + } + + fn totp(secret_bytes: &[u8]) -> u64 { + oath::totp_raw_now(&secret_bytes, 6, 0, 30, &oath::HashType::SHA1) + } +} + +struct Cli { + app: RusTOTPony, +} + +impl Cli { + fn new() -> Self { + let db = JsonDatabase::new(Self::get_database_path()); + Self { + app: RusTOTPony::new(db), + } + } + + fn run(&mut self) { + match self.get_cli_api_matches().subcommand() { + ("dash", Some(_)) => { + self.show_dashboard(); + } + ("list", Some(_)) => { + self.show_applications_list(false); + } + ("show-all", Some(_)) => { + self.show_applications_list(true); + } + ("show", Some(sub_app)) => { + let app_name: &str = sub_app + .value_of("APPNAME") + .expect("Couldn't read APPNAME for 'show' command"); + self.show_application(app_name); + } + ("add", Some(sub_app)) => { + let app_name: &str = sub_app + .value_of("APPNAME") + .expect("Couldn't read APPNAME for 'add' command"); + let key: &str = sub_app + .value_of("KEY") + .expect("Couldn't read KEY for 'add' command"); + self.add_application(app_name, key); + } + ("delete", Some(sub_app)) => { + let app_name: &str = sub_app + .value_of("APPNAME") + .expect("Couldn't read APPNAME for 'delete' command"); + self.delete_application(app_name); + } + ("rename", Some(sub_app)) => { + let app_name: &str = sub_app + .value_of("APPNAME") + .expect("Couldn't read APPNAME for 'rename' command"); + let new_name: &str = sub_app + .value_of("NEWNAME") + .expect("Couldn't read NEWNAME for 'rename' command"); + self.rename_application(app_name, new_name); + } + ("eradicate", Some(_)) => { + self.eradicate_database(); + } + _ => { + self.show_applications_list(true); + } + } + } + + fn get_cli_api_matches(&self) -> clap::ArgMatches<'static> { + App::new("Main") + .version("0.1.0") + .author("German Lashevich ") + .about("TOTP code generator written with Rust") + .subcommand( + SubCommand::with_name("dash").about("Shows realtime dashboard with all generators"), + ) + .subcommand(SubCommand::with_name("list").about("Lists all generators")) + .subcommand( + SubCommand::with_name("show-all") + .about("Shows all generators with their's current values"), + ) + .subcommand( + SubCommand::with_name("show") + .about("Shows generator with it's current value") + .arg(Arg::with_name("APPNAME").required(true)), + ) + .subcommand( + SubCommand::with_name("add") + .about("Adds new generator") + .arg(Arg::with_name("APPNAME").required(true)) + .arg(Arg::with_name("KEY").required(true)), + ) + .subcommand( + SubCommand::with_name("delete") + .about("Deletes generator") + .arg(Arg::with_name("APPNAME").required(true)), + ) + .subcommand( + SubCommand::with_name("rename") + .about("Renames generator") + .arg(Arg::with_name("APPNAME").required(true)) + .arg(Arg::with_name("NEWNAME").required(true)), + ) + .subcommand(SubCommand::with_name("eradicate").about("Deletes all generators")) + .get_matches() + } + + fn get_database_path() -> PathBuf { + let home = std::env::home_dir().unwrap_or(PathBuf::from(".")); + home.join(Path::new(CONFIG_PATH)) + } + + fn show_dashboard(&self) { + match self.app.get_applications(){ + Ok(apps) => { + let mut is_first_iteration = true; + let lines_count = apps.len(); + loop { + if is_first_iteration { + is_first_iteration = false; + } else { + print!("\x1B[{}A", lines_count); + } + for (_, app) in apps { + println!{"{} {}", app.get_code(), app.name}; + } + thread::sleep(Duration::from_millis(1000)); + } + }, + Err(err) => println!("{}", err), + } + } + + fn show_applications_list(&self, _: bool) { + // TODO Create Table structure with HashMap as follows and metadata about columns - width, titles, names + let mut output_table: HashMap<&str, Vec> = HashMap::new(); + let mut applications_count = 0; + let apps = match self.app.get_applications() { + Ok(v) => v, + Err(e) => { + println!("{}", e); + return; + }, + }; + for (_, application) in apps { + applications_count += 1; + output_table + .entry("name") + .or_insert(Vec::new()) + .push(application.name.clone()); + output_table + .entry("key") + .or_insert(Vec::new()) + .push(application.secret.clone()); + output_table + .entry("username") + .or_insert(Vec::new()) + .push(application.username.clone()); + } + let name_max_length = output_table["name"] + .iter() + .fold("name".len(), |max, val| std::cmp::max(max, val.len())); + let key_max_length = output_table["key"] + .iter() + .fold("key".len(), |max, val| std::cmp::max(max, val.len())); + let username_max_length = output_table["username"] + .iter() + .fold("username".len(), |max, val| std::cmp::max(max, val.len())); + let header_row_delimiter = format!( + "+-{}-+-{}-+-{}-+", + "-".repeat(name_max_length), + "-".repeat(key_max_length), + "-".repeat(username_max_length) + ); + + println!("{}", header_row_delimiter); + println!( + "| {name: { + self.app.flush(); + println!("New application created: {}", name) + }, + Err(err) => println!("{} Aborting…", err), + } + } + + fn delete_application(&mut self, name: &str) { + match self.app.delete_application(name) { + Ok(_) => { + self.app.flush(); + println!("Application '{}' successfully deleted", name) + }, + Err(err) => println!("Couldn't delete application '{}': {}", name, err), + }; + } + + fn rename_application(&mut self, name: &str, newname: &str) { + match self.app.rename_application(name, newname) { + Ok(_) => { + self.app.flush(); + println!("Application '{}' successfully renamed to '{}'", name, newname) + }, + Err(err) => println!("Couldn't rename application '{}': {}", name, err), + }; + } + + fn eradicate_database(&mut self) { + self.app.delete_all_applications(); + self.app.flush(); + println!("Done."); + } +} + +fn main() { + Cli::new().run(); +}