pwtool-0.9.3/.cargo_vcs_info.json0000644000000001360000000000100123710ustar { "git": { "sha1": "1d154d6e4104f9b51eae560b4523e1b437b06dc1" }, "path_in_vcs": "" }pwtool-0.9.3/.gitlab-ci.yml000064400000000000000000000004731046102023000136210ustar 00000000000000image: "rust:latest" before_script: - apt-get update -yqq - apt-get install -yqq --no-install-recommends build-essential pandoc # Use cargo to test the project test:cargo: script: | rustc --version && cargo --version # Print version info for debugging cargo test --workspace --verbose make pwtool-0.9.3/Cargo.lock0000644000000241740000000000100103540ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "base32" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ "generic-array", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "blowfish" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fa6a061124e37baba002e496d203e23ba3d7b73750be82dbfbc92913048a5b" dependencies = [ "byteorder", "cipher", "opaque-debug", ] [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cipher" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" dependencies = [ "generic-array", ] [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "crypto-mac" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" dependencies = [ "generic-array", "subtle", ] [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ "generic-array", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", "subtle", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getopts" version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ "crypto-mac", "digest 0.9.0", ] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest 0.10.7", ] [[package]] name = "libc" version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "md-5" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", "opaque-debug", ] [[package]] name = "md5" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", "hmac 0.12.1", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "pwhash" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56" dependencies = [ "blowfish", "byteorder", "hmac 0.10.1", "md-5", "rand", "sha-1", "sha2 0.9.9", ] [[package]] name = "pwtool" version = "0.9.3" dependencies = [ "base32", "base64", "getopts", "hex", "hmac 0.12.1", "md5", "pbkdf2", "pwhash", "rand", "sha1", "sha2 0.10.9", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "sha-1" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", "digest 0.10.7", ] [[package]] name = "sha2" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", "cpufeatures", "digest 0.9.0", "opaque-debug", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", "digest 0.10.7", ] [[package]] name = "subtle" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "typenum" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-width" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", "syn", ] pwtool-0.9.3/Cargo.toml0000644000000025130000000000100103700ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "pwtool" version = "0.9.3" authors = ["ed neville "] description = "pwtool, user account password tool" homepage = "https://www.usenix.org.uk/content/pwtool.html" documentation = "https://www.usenix.org.uk/content/pwtool.html" readme = "README.md" keywords = [ "password", "util", "hash", ] categories = ["command-line-utilities"] license = "GPL-3.0-or-later" repository = "https://gitlab.com/edneville/pwtool" [dependencies.base32] version = "0.5" [dependencies.base64] version = "0.22" [dependencies.getopts] version = "0.2" [dependencies.hex] version = "0.4" [dependencies.hmac] version = "0.12" [dependencies.md5] version = "0.7" [dependencies.pbkdf2] version = "0.12" [dependencies.pwhash] version = "1" [dependencies.rand] version = "0.8" [dependencies.sha1] version = "0.10" [dependencies.sha2] version = "0.10" pwtool-0.9.3/Cargo.toml.orig000064400000000000000000000013211046102023000140450ustar 00000000000000[package] name = "pwtool" version = "0.9.3" edition = "2018" description = "pwtool, user account password tool" authors = ["ed neville "] license = "GPL-3.0-or-later" homepage = "https://www.usenix.org.uk/content/pwtool.html" repository = "https://gitlab.com/edneville/pwtool" readme = "README.md" categories = ["command-line-utilities"] documentation = "https://www.usenix.org.uk/content/pwtool.html" keywords = ["password","util","hash"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] rand = "0.8" pwhash = "1" getopts = "0.2" sha1 = "0.10" md5 = "0.7" pbkdf2 = "0.12" base64 = "0.22" sha2 = "0.10" hmac = "0.12" hex = "0.4" base32 = "0.5" pwtool-0.9.3/Makefile000064400000000000000000000014651046102023000126270ustar 00000000000000# vim:set noet: NAME := pwtool DOC := $(NAME) VERSION := $(shell grep ^version Cargo.toml | sed -e 's/.* = "//g;s/"$$//g' ) MDDATE := $(shell find $(NAME).md -printf "%Td %TB %TY\n" ) RELEASE := ./target/release/$(NAME) all: build test bintest doc build: cargo build --release doc: ( cat $(DOC).md | sed -e 's/^footer: .*$$/footer: $(NAME) $(VERSION)/g' -e 's/^date:.*/date: $(MDDATE)/g' ) > $(DOC).md.tmp && mv $(DOC).md.tmp $(DOC).md cat $(DOC).md | sed -e 's,\([^ `-]\)--\([a-zA-Z]\),\1\\--\2,g' -e '/^|/s/\\n/\\\\n/g' -e '/^|/s/\\t/\\\\t/g' > $(DOC).man.md pandoc --standalone --ascii --to man $(DOC).man.md -o $(DOC).1 rm $(DOC).man.md test: cargo test bintest: install: all command -v please && please install -m 0755 -s $(RELEASE) /usr/local/bin || sudo install -m 0755 -s $(RELEASE) /usr/local/bin pwtool-0.9.3/README.md000064400000000000000000000151511046102023000124430ustar 00000000000000# pwtool Generate passwords from random characters or words and optionally show their cryptographic hash. The default generated password set is copy/paste friendly without extended characters that would break the default copy selection you get when double-clicking a word. They also are don't break quotation strings (quote marks, double quotes or backticks). # installing From cargo, ensuring that the install path (normally ~/.cargo/bin) is in your `PATH` environment. ``` cargo install pwtool ``` source code ``` git clone https://gitlab.com/edneville/pwtool.git cd pwtool cargo build --release ``` please or sudo ``` please install -m0755 -o0 -g0 target/release/pwtool /usr/local/bin/ ``` snap ``` please snap install pwtool ``` # modifiers `lowercase`, `uppercase`, `numeric` and `extended` will set requirements on the passwords. # word lists If you want a password generated from words rather than a mixture of letters and numbers, use the `words` option, which by default uses the file `/usr/share/dict/words`. Use `wordsfile` to specify a different list. # cryptographic hash The `--md5`, `--bcrypt`, `--des`, `--sha1`, `--sha256` and `--sha512` options will print the cryptographic hash which can be used instead of storing the password in some systems. The hash output can be used with `useradd`/`usermod` or `.htaccess`: ``` LINE=`pwtool --number 1 --sha256` PW="${LINE% *}" HASH="${LINE##* }" USR=newuser useradd -m -p "$HASH" -s /bin/bash $USR echo "Password for $USR is $PW" ``` Or issue a new password to an existing user with `usermod`: ``` LINE=`pwtool --number 1 --sha256` PW=`echo "$LINE" | sed -e 's/ .*//g'` HASH=`echo "$LINE" | sed -e 's/.* //g'` USR=newuser usermod -p "$HASH" $USR echo "Password for $USR is now $PW" ``` # options `--userfmt`, `--mysqlfmt`, `--pgfmt`, `--htauthfmt`, `--usermodfmt`, `--totpfmt` are convenience options for format variables below, with the exception that they will also print the password in a comment: pwtool --username thingy --userfmt This then outputs like this: useradd -m -s /bin/bash -p '$5$YLtTnPhYiQ891nAz$SHzSCc5vMIARxd4PYtxIOZ7mGICNsLGEGimMyFpRjE7' thingy # 8OtQUoUjV9 `--createdatabase` when combined with one of the database options will prefix with a `create database %{database};` string. The `--totp`, `--totpstep` options are for the key (base32), length of digits and step. # format strings With `--format` the variables can be used to output a custom string. The variables (below) can be used within a `--format` string to output in a convenient way: pwtool --username thingy --format "useradd -m -s /bin/bash -p '%{sha256}' %{username} # %{password}\n" This then outputs like this: useradd -m -s /bin/bash -p '$5$YLtTnPhYiQ891nAz$SHzSCc5vMIARxd4PYtxIOZ7mGICNsLGEGimMyFpRjE7' thingy # 8OtQUoUjV9 You can then copy/paste that around different systems where people need the same account. Another common way is to use it for mysql setup at the same time: pwtool --username thingy --database thing --format "grant all privileges on %{database}.* to %{username}@'%' identified with mysql_native_password as '%{mysql}';\n"; | variable | output | |----------|--------| | %{des} | traditional crypt | | %{bcrypt} | BSD standard hash | | %{md5} | MD5 hash | | %{sha1} | HMAC SHA1 | | %{sha256} | SHA256 | | %{sha512} | SHA512 | | %{mysql} | password in mysql_native format | | %{password} | cleartext password | | %{username} | placeholder for --username | | %{database} | placeholder for --database | | %{postgres} | postgres SCRAM-SHA-256 password | | %{userfmt} | expands to `useradd -m -s /bin/bash -p '%{sha256}' %{username}` | | %{usermodfmt} | expands to `usermod -p '%{sha256}' %{username}` | | %{mysqlfmt} | expands to `grant all privileges on %{database}.* to %{username}@'%' identified with mysql_native_password as '%{mysql}';` | | %{mysqluserfmt} | expands to `create user %{username}@'%'; alter user %{username}@'%' identified with 'caching_sha2_password' as %{mysql_caching_sha2}; grant all privileges on %{database}.* to %{username}@'%';` | | %{pgfmt} | expands to `create user %{username} password '%{postgres}';` | | %{htauthfmt} | expands to `%{username}:%{sha256}` | | %{totp} | displays the current TOTP SHA1 | | %{totpfmt} | an alias for the TOTP number and remaining time progress | | %{totpsecs} | displays the remaining seconds | | %{totpsecsmax} | displays the max sec step | | %{totpprogress} | displays a progress bar for remaining secs | | %{totpalgo} | displays the TOTP algorithm used | # executing output Should you want to execute the output, `tee` is quite handy as it will print to stdout and an elevated file descriptor: pwtool --username moo --format "useradd -m -s /bin/bash -p '%{sha256}' %{username} # %{password}\n" --number 1 | tee >( please /bin/bash ) useradd -m -s /bin/bash -p '$5$pZxFddWqXpBuozZF$l1Eyw2HqsGP0E9pdQctqeCPTOL3eJOPq4pNiI6MoZG5' moo # 6nIFhAKJSC This will both add the user and print the password in the shell. # http basic authentication You can populate entries in basic authentication files too: pwtool --username moo --format '%{htauthfmt}' Not the output has the password in a comment prior to the auth line as data after the `:` is normally treated as the hashed password. You can store this in `/etc/apache2/restricted`: AuthType Basic AuthName "Keep out!" AuthUserFile "/etc/apache2/restricted" Require valid-user or with nginx, in `/etc/nginx/restricted`: auth_basic "Keep out!"; auth_basic_user_file /etc/nginx/restricted; # as a totp authenticator program If you want to leave a TOTP authentication display in your terminal, it can run like this: TOTP=metalisbest pwtool --totpfmt --loopdelay 1 It will then run and leave a display like this: 202924 [######################### ] If you have multiple accounts, they can be displayed like this: TOTP="metalisbest;grungeisbest" pwtool --totpfmt --loopdelay 1 617989 [################## ] 826783 [################## ] That doesn't show you the names, so you can include that with key=value strings: TOTP="key=metalisbest,name=metal;key=grungeisbest,name=grunge" pwtool --format "%{totpfmt} %{username}\n" --loopdelay 1 488833 [################# ] metal 967495 [################# ] grunge The following key=value pairs are supported: | key | definition | |----------|--------| | key / totp | the totp string | | name / username | a meaningful name for this key | | step | number of step seconds | | digits | the length of the output | | algo | which hmac to use (sha1, sha256, sha512) | | seconds | a user-defined time | pwtool-0.9.3/changelog.md000064400000000000000000000024021046102023000134300ustar 000000000000000.9.2 * number defaults to 1 when totp is set 0.9.1 * multiple totp authentication strings in --totp * colourise totp progress bar * enable looping 0.9.0 * support for SHA1/SHA256/SHA512 totp algorithms * --length applies to totp generation now, --totplen removed as a result 0.8.1 * totp progress bar * --totpfmt option 0.8.0 * basic totp functions (--totpkey, --totpstep, --totplen) * allow predefining the password with --password * allow --totpkey and --password to be supplied as environment variables by the same name 0.7.0 * use --sha512/256/1 --md5 --des --bcrypt in --fmt argument * new options to output vhosts for nginx and apache * when outputting creating postgres database, include alter owner * when creating postgres database, include alter owner * mysqluserfmt uses caching sha2 mysql format 0.6.1 * --onlylowercase and --onlyuppercase convert --words options to desired case * --nospaces option for --words 0.6.0 * change convenience variables to not include \n * new options --htauthfmt --mysqlfmt --pgfmt --userfmt --usermodfmt 0.5.0 * correct naming of htauth -> htauthfmt to match other similar helpers 0.4.1 * default convenience formatting for mysql, postgres, htauth and useradd commands * docs and tests 0.4.0 * mysql and postgres pwtool-0.9.3/pwtool.1000064400000000000000000000073131046102023000125730ustar 00000000000000'\" t .\" Automatically generated by Pandoc 3.1.11.1 .\" .TH "pwtool" "1" "15 August 2025" "pwtool 0.9.3" "User Manual" .SH NAME pwtool \- a convenience tool to make sane passwords and account creations .SH SYNOPSIS \f[B]pwtool\f[R] .PP \f[B]pwtool \[en]number N\f[R] .PP \f[B]pwtool \[en]length N\f[R] .PP \f[B]pwtool \[en][only]alpha\f[R] .PP \f[B]pwtool \[en][only]numeric\f[R] .PP \f[B]pwtool \[en][only]extended\f[R] .PP \f[B]pwtool \[en][only]lowercase\f[R] .PP \f[B]pwtool \[en][only]uppercase\f[R] .PP \f[B]pwtool \[en]md5\f[R] .PP \f[B]pwtool \[en]des\f[R] .PP \f[B]pwtool \[en]bcrypt\f[R] .PP \f[B]pwtool \[en]sha[1,256,512]\f[R] .PP \f[B]pwtool \[en]username name\f[R] .PP \f[B]pwtool \[en]database name\f[R] .PP \f[B]pwtool \[en]createdatabase\f[R] .PP \f[B]pwtool \[en]password STRING\f[R] .PP \f[B]pwtool \[en]totp STRING\f[R] .PP \f[B]pwtool \[en]totpstep NUMBER\f[R] .SH DESCRIPTION \f[B]pwtool\f[R] is a utility to add generate account passwords in a variety of formats with helper output on \f[B]stdout\f[R]. By default the password strings are made of of letters and numbers for easy mouse selection. .PP As well as flexible password generation options a main goal is to output user creation strings to copy and paste/execute as stdin so that operators don\[cq]t have to re\-type passwords. .PP Crypts can be based on user supplied strings via the \f[B]\-\-password\f[R] option or \f[B]PASSWORD\f[R] environment variable. .PP The \f[B]\-\-format\f[R] string can expand values: .IP .EX pwtool \-\-database billing \-\-username wonkeydonkey \-\-password hunter2 \-\-servername webby \-\-number 1 \-\-format \[aq]DB: %{database}\[rs]\[rs]nUSR: %{username}\[rs]\[rs]nPASSWORD: %{password}\[rs]\[rs]nSERVER: %{servername}\[rs]\[rs]n\[aq] .EE .PP Other strings, such as \f[B]\-\-mysqlfmt\f[R], \f[B]\-\-pgfmt\f[R], \f[B]\-\-mysqluserfmt\f[R], \f[B]\-\-userfmt\f[R], can generate copy/paste shell commands: .IP .EX pwtool \-\-username wonkeydonkey \-\-number 1 \-\-userfmt useradd \-m \-s /bin/bash \-p \[aq]$5$hYhnxam4j/chBu3V$BsZsRl4nj6DTpEdFMfLuerPFR0xvCJmeGQCUjuG9qM1\[aq] wonkeydonkey # Y9YgmSyv1A pwtool \-\-username wonkeydonkey \-\-database circus \-\-createdatabase \-\-number 1 \-\-mysqlfmt create database circus; grant all privileges on circus.* to wonkeydonkey\[at]\[aq]%\[aq] identified with mysql_native_password as \[aq]*21c0a42c1bb43ff6b56226a6a65a8859dd077497\[aq]; \-\- # VfKptnR2ft .EE .SH TOTP \f[B]pwtool\f[R] can work as a TOTP CLI and display TOTP authentication strings. .PP If you want to leave a TOTP authentication display in your terminal, it can run like this: .IP .EX TOTP=metalisbest pwtool \-\-totpfmt \-\-loopdelay 1 .EE .PP It will then run and leave a display like this: .IP .EX 202924 [######################### ] .EE .PP If you have multiple accounts, they can be displayed like this: .PP TOTP=\[lq]metalisbest;grungeisbest\[rq] pwtool \[en]totpfmt \[en]loopdelay 1 .IP .EX 617989 [################## ] 826783 [################## ] .EE .PP That doesn\[cq]t show you the names, so you can include that with key=value strings: .IP .EX TOTP=\[dq]key=metalisbest,name=metal;key=grungeisbest,name=grunge\[dq] pwtool \-\-format \[dq]%{totpfmt} %{username}\[rs]n\[dq] \-\-loopdelay 1 488833 [################# ] metal 967495 [################# ] grunge .EE .PP The following key=value pairs are supported: .PP .TS tab(@); l l. T{ key T}@T{ definition T} _ T{ key / totp T}@T{ the totp string T} T{ name / username T}@T{ a meaningful name for this key T} T{ step T}@T{ number of step seconds T} T{ digits T}@T{ the length of the output T} T{ algo T}@T{ which hmac to use (sha1, sha256, sha512) T} T{ seconds T}@T{ a user\-defined time T} .TE .SH AUTHORS Ed Neville (ed\-pwtool\[at]s5h.net). pwtool-0.9.3/pwtool.md000064400000000000000000000062441046102023000130350ustar 00000000000000--- title: pwtool section: 1 header: User Manual footer: pwtool 0.9.3 author: Ed Neville (ed-pwtool@s5h.net) date: 15 August 2025 --- # NAME pwtool - a convenience tool to make sane passwords and account creations # SYNOPSIS **pwtool** **pwtool --number N** **pwtool --length N** **pwtool --[only]alpha** **pwtool --[only]numeric** **pwtool --[only]extended** **pwtool --[only]lowercase** **pwtool --[only]uppercase** **pwtool --md5** **pwtool --des** **pwtool --bcrypt** **pwtool --sha[1,256,512]** **pwtool --username [name]** **pwtool --database [name]** **pwtool --createdatabase** **pwtool --password STRING** **pwtool --totp STRING** **pwtool --totpstep NUMBER** # DESCRIPTION **pwtool** is a utility to add generate account passwords in a variety of formats with helper output on **stdout**. By default the password strings are made of of letters and numbers for easy mouse selection. As well as flexible password generation options a main goal is to output user creation strings to copy and paste/execute as stdin so that operators don't have to re-type passwords. Crypts can be based on user supplied strings via the **--password** option or **PASSWORD** environment variable. The **--format** string can expand values: pwtool --database billing --username wonkeydonkey --password hunter2 --servername webby --number 1 --format 'DB: %{database}\\nUSR: %{username}\\nPASSWORD: %{password}\\nSERVER: %{servername}\\n' Other strings, such as **--mysqlfmt**, **--pgfmt**, **--mysqluserfmt**, **--userfmt**, can generate copy/paste shell commands: pwtool --username wonkeydonkey --number 1 --userfmt useradd -m -s /bin/bash -p '$5$hYhnxam4j/chBu3V$BsZsRl4nj6DTpEdFMfLuerPFR0xvCJmeGQCUjuG9qM1' wonkeydonkey # Y9YgmSyv1A pwtool --username wonkeydonkey --database circus --createdatabase --number 1 --mysqlfmt create database circus; grant all privileges on circus.* to wonkeydonkey@'%' identified with mysql_native_password as '*21c0a42c1bb43ff6b56226a6a65a8859dd077497'; -- # VfKptnR2ft # TOTP **pwtool** can work as a TOTP CLI and display TOTP authentication strings. If you want to leave a TOTP authentication display in your terminal, it can run like this: TOTP=metalisbest pwtool --totpfmt --loopdelay 1 It will then run and leave a display like this: 202924 [######################### ] If you have multiple accounts, they can be displayed like this: TOTP="metalisbest;grungeisbest" pwtool --totpfmt --loopdelay 1 617989 [################## ] 826783 [################## ] That doesn't show you the names, so you can include that with key=value strings: TOTP="key=metalisbest,name=metal;key=grungeisbest,name=grunge" pwtool --format "%{totpfmt} %{username}\n" --loopdelay 1 488833 [################# ] metal 967495 [################# ] grunge The following key=value pairs are supported: | key | definition | |----------|--------| | key / totp | the totp string | | name / username | a meaningful name for this key | | step | number of step seconds | | digits | the length of the output | | algo | which hmac to use (sha1, sha256, sha512) | | seconds | a user-defined time | pwtool-0.9.3/src/lib.rs000064400000000000000000000742141046102023000130740ustar 00000000000000use base32::*; use base64::prelude::*; use hmac::{Hmac, Mac}; use pbkdf2::pbkdf2_hmac; use pwhash::*; use rand::distributions::Alphanumeric; use rand::thread_rng; use rand::Rng; use sha1::{Digest, Sha1}; use sha2::Sha256; use sha2::Sha512; use std::fs::File; use std::io::BufRead; use std::io::BufReader; use std::thread; use std::time::Duration; use std::time::SystemTime; use std::time::UNIX_EPOCH; #[derive(Clone)] pub enum TotpAlgo { Sha1, Sha256, Sha512, } #[derive(Clone)] pub struct Config { pub number: u32, pub len: Option, pub pw_type: Option, pub word_list: Option, pub words: Option>, pub username: Option, pub database: Option, pub create_database: bool, pub spaces: bool, pub digest: Option, pub servername: Option, pub totp_key: Option, pub totp_step: Option, pub totp_algo: TotpAlgo, pub totp_seconds: Option, pub password: Option, pub loopdelay: Option, } impl Config { pub fn new() -> Config { Config { len: None, pw_type: None, number: 20, word_list: None, words: None, username: None, database: None, create_database: false, spaces: true, digest: None, servername: None, totp_key: None, totp_step: Some(30), totp_seconds: None, password: None, totp_algo: TotpAlgo::Sha1, loopdelay: None, } } pub fn digest(&self) -> String { if self.digest.is_none() { return "sha256".to_string(); } self.digest.as_ref().unwrap().to_string() } pub fn totp_seconds(&self) -> u64 { if self.totp_seconds.is_some() { self.totp_seconds.unwrap() } else { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() } } } impl Default for Config { fn default() -> Self { Self::new() } } pub enum PwClass { Num = 1 << 0, Alpha = 1 << 1, Ext = 1 << 2, Lower = 1 << 3, Upper = 1 << 4, } pub fn valid_word(s: &str, word_set: Option) -> bool { if word_set.is_some() { let mut valid = false; if word_set.as_ref().unwrap() & PwClass::Num as u32 != 0 { for j in s.chars() { if j.is_ascii_digit() { valid = true; } } } if word_set.as_ref().unwrap() & PwClass::Lower as u32 != 0 { for j in s.chars() { if j.is_ascii_lowercase() { valid = true; } } } if word_set.as_ref().unwrap() & PwClass::Upper as u32 != 0 { for j in s.chars() { if j.is_ascii_uppercase() { valid = true; } } } if word_set.as_ref().unwrap() & PwClass::Ext as u32 != 0 { for j in s.chars() { if !j.is_ascii_digit() && !j.is_ascii_lowercase() && !j.is_ascii_uppercase() { valid = true; } } } return valid; }; true } pub fn prng_string(c: &Config) -> String { let mut rng = rand::thread_rng(); if c.word_list.is_some() { let mut phrase = vec![]; let words_len = c.words.as_ref().unwrap().len(); if c.words.as_ref().unwrap().is_empty() { eprintln!("no words to process"); std::process::exit(1); } for _j in 0..c.len.unwrap_or(15) { phrase.push( c.words.as_ref().unwrap()[rng.gen_range(0..words_len)] .clone() .to_string(), ); } let mut phrase = phrase.join(if c.spaces { " " } else { "" }); if c.pw_type.is_some() { let set = c.pw_type.as_ref().unwrap(); if set & PwClass::Lower as u32 != 0 { phrase = phrase.to_lowercase(); } if set & PwClass::Upper as u32 != 0 { phrase = phrase.to_uppercase(); } } return phrase; } let mut set = c.pw_type; if set.is_none() { set = Some( PwClass::Num as u32 | PwClass::Alpha as u32 | PwClass::Lower as u32 | PwClass::Upper as u32, ); } let set = set.unwrap(); let mut chars = "".to_string(); if set & PwClass::Num as u32 != 0 { chars += "0123456789"; }; if set & PwClass::Alpha as u32 != 0 { chars += "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; }; if set & PwClass::Lower as u32 != 0 { chars += "abcdefghijklmnopqrstuvwxyz"; }; if set & PwClass::Upper as u32 != 0 { chars += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; }; if set & PwClass::Ext as u32 != 0 { chars += r#"!"$%^&*()-={}[]:;@'<>,./\|"#; }; let one_char = || chars.chars().nth(rng.gen_range(0..chars.len())).unwrap(); std::iter::repeat_with(one_char) .take(c.len.unwrap_or(15) as usize) .collect() } pub fn gen_salt_str(chars: usize) -> String { let rng = thread_rng(); rng.sample_iter(&Alphanumeric) .take(chars) .map(|x| x as char) .collect() } pub fn postgres_pass(pw: &str) -> String { let salt_size = 16; let iterations = 4096; let salt = gen_salt_str(salt_size); let mut key = [0u8; 32]; pbkdf2_hmac::(pw.as_bytes(), salt.as_bytes(), iterations, &mut key); let mut client_key = Hmac::::new_from_slice(&key).unwrap(); client_key.update(b"Client Key"); let client_result = client_key.finalize(); let mut server_key = Hmac::::new_from_slice(&key).unwrap(); server_key.update(b"Server Key"); let server_result = server_key.finalize(); let mut stored_key = Sha256::new(); stored_key.update(client_result.clone().into_bytes()); let stored_key = stored_key.finalize(); let bstored_key = BASE64_STANDARD.encode(stored_key); let bsalt = BASE64_STANDARD.encode(salt); let bserver_key = BASE64_STANDARD.encode(server_result.clone().into_bytes()); format!("SCRAM-SHA-256${iterations}:{bsalt}${bstored_key}:{bserver_key}") } pub fn mysql_pass(pw: &str) -> String { let mut h = Sha1::new(); h.update(pw); let r = h.finalize(); let mut h = Sha1::new(); h.update(r); let r = h.finalize(); format!("*{:x}", r).to_string() } // https://github.com/ansible-collections/community.mysql/issues/621#issuecomment-2051837825 pub fn to64(mut v: i32, n: i32) -> String { let mut index64: Vec = vec![]; index64.push(b'.'); index64.push(b'/'); for i in 48..58 { index64.push(i); } for i in 65..91 { index64.push(i); } for i in 97..123 { index64.push(i); } let mut result = "".to_string(); // make a vec of u8 let mut nn = n; while nn > 0 { nn -= 1; result.push(index64[v as usize & 0x3f] as char); v >>= 6; } String::from_utf8_lossy(result.as_bytes()).to_string() } pub fn mysql_caching_sha2_pass_salt(pw: &str, salt: &str, iterations: i32) -> String { let num_bytes = 32; let key = pw.as_bytes(); let salt = salt.as_bytes(); let mut d1: Vec = vec![]; d1.extend(key); d1.extend(salt); d1.extend(key); let mut stored_key = Sha256::new(); stored_key.update(d1); let digest_b = stored_key.clone().finalize(); let mut tmp: Vec = vec![]; tmp.extend(key); tmp.extend(salt); for i in (0..=key.len()).rev().step_by(num_bytes) { tmp.extend(if i > num_bytes { digest_b.as_slice() } else { &digest_b[0..i] }); } let mut i = key.len(); while i > 0 { tmp.extend(if i & 1 != 0 { digest_b.as_slice() } else { key as &[u8] }); i >>= 1; } let mut digest_a_hash = Sha256::new(); digest_a_hash.update(tmp); let digest_a = digest_a_hash.clone().finalize(); let mut tmp: Vec = vec![]; for _ in 0..key.len() { tmp.extend(key); } let mut digest_dp_hash = Sha256::new(); digest_dp_hash.update(tmp); let digest_dp = digest_dp_hash.finalize(); let mut byte_sequence_p: Vec = vec![]; for i in (0..=key.len()).rev().step_by(num_bytes) { byte_sequence_p.extend(if i > num_bytes { digest_dp.as_slice() } else { &digest_dp[0..i] }); } let mut tmp: Vec = vec![]; let til = 16 + (digest_a[0] as i32); for _ in 0..til { tmp.extend(salt); } let mut digest_ds_hash = Sha256::new(); digest_ds_hash.update(tmp); let digest_ds = digest_ds_hash.finalize(); let mut byte_sequence_s: Vec = vec![]; for i in (0..=salt.len()).rev().step_by(num_bytes) { byte_sequence_s.extend(if i > num_bytes { digest_ds.as_slice() } else { &digest_ds[0..i] }); } let mut digest_c = digest_a; for i in 0..iterations * 1000 { tmp = if i & 1 > 0 { byte_sequence_p.clone() } else { (&digest_c as &[u8]).to_vec() }; if i % 3 > 0 { tmp.extend(byte_sequence_s.clone()); } if i % 7 > 0 { tmp.extend(byte_sequence_p.clone()); } tmp.extend(if i & 1 > 0 { digest_c.as_slice() } else { &byte_sequence_p as &[u8] }); let mut digest_c_hash = Sha256::new(); digest_c_hash.update(tmp); digest_c = digest_c_hash.finalize(); } let inc1 = 10; let inc2 = 21; let m = 30; let end = 0; let mut i = 0; let mut tmp = "".to_string(); loop { tmp.push_str(&to64( ((digest_c[i] as i32) << 16) | (((digest_c[(i + inc1) % m]) as i32) << 8) | (digest_c[(i + inc1 * 2) % m]) as i32, 4, )); i = (i + inc2) % m; if i == end { break; } } tmp.push_str(&to64( ((digest_c[31] as i32) << 8) | (digest_c[30] as i32), 3, )); tmp } pub fn mysql_caching_sha2_pass(pw: &str) -> String { let salt_size = 20; let salt = gen_salt_str(salt_size); let count = 5; let st = mysql_caching_sha2_pass_salt(pw, &salt, count); format!( "0x{}", hex::encode(format!("$A${count:>03}${salt}{st}")).to_uppercase() ) } pub fn useradd_format(c: &Config) -> String { let digest = c.digest(); format!("useradd -m -s /bin/bash -p '%{{{digest}}}' %{{username}}").to_string() } pub fn usermod_format(c: &Config) -> String { let digest = c.digest(); format!("usermod -p '%{{{digest}}}' %{{username}}").to_string() } pub fn mysql_format() -> String { "grant all privileges on %{database}.* to %{username}@'%' identified with mysql_native_password as '%{mysql}';".to_string() } pub fn mysql_user_format() -> String { "create user %{username}@'%'; alter user %{username}@'%' identified with 'caching_sha2_password' as %{mysql_caching_sha2}; grant all privileges on %{database}.* to %{username}@'%';".to_string() } pub fn postgres_format() -> String { "create user %{username} password '%{postgres}';".to_string() } pub fn htauth_format(c: &Config) -> String { let digest = if c.digest.is_none() { "md5".to_string() } else { c.digest() }; format!("%{{username}}:%{{{digest}}}").to_string() } #[allow(deprecated)] pub fn process_format_string(format_string: &mut String, c: &Config, pw: &str) -> String { if format_string.contains("%{userfmt}") { *format_string = format_string.replace("%{userfmt}", &useradd_format(c).to_string()); } if format_string.contains("%{usermodfmt}") { *format_string = format_string.replace("%{usermodfmt}", &usermod_format(c).to_string()); } if format_string.contains("%{mysqlfmt}") { *format_string = format_string.replace("%{mysqlfmt}", &mysql_format().to_string()); } if format_string.contains("%{mysqluserfmt}") { *format_string = format_string.replace("%{mysqluserfmt}", &mysql_user_format().to_string()); } if format_string.contains("%{pgfmt}") { *format_string = format_string.replace("%{pgfmt}", &postgres_format().to_string()); } if format_string.contains("%{htauthfmt}") { *format_string = format_string.replace("%{htauthfmt}", &htauth_format(c).to_string()); } if format_string.contains("%{totpfmt}") { *format_string = format_string.replace("%{totpfmt}", "%{totp} [%{totpprogress}]"); } if format_string.contains("%{md5}") { *format_string = format_string.replace("%{md5}", &md5_crypt::hash(pw).unwrap().to_string()); } if format_string.contains("%{bcrypt}") { *format_string = format_string.replace("%{bcrypt}", &bcrypt::hash(pw).unwrap().to_string()); } if format_string.contains("%{des}") { *format_string = format_string.replace("%{des}", &unix_crypt::hash(pw).unwrap().to_string()); } if format_string.contains("%{sha1}") { *format_string = format_string.replace("%{sha1}", &sha1_crypt::hash(pw).unwrap().to_string()); } if format_string.contains("%{sha256}") { *format_string = format_string.replace("%{sha256}", &sha256_crypt::hash(pw).unwrap().to_string()); } if format_string.contains("%{sha512}") { *format_string = format_string.replace("%{sha512}", &sha512_crypt::hash(pw).unwrap().to_string()); } if format_string.contains("%{password}") { *format_string = format_string.replace("%{password}", pw); } if format_string.contains("%{username}") && c.username.is_some() { *format_string = format_string.replace("%{username}", c.username.as_ref().unwrap()); } if format_string.contains("%{database}") && c.database.is_some() { *format_string = format_string.replace("%{database}", c.database.as_ref().unwrap()); } if format_string.contains("%{mysql}") { *format_string = format_string.replace("%{mysql}", &mysql_pass(pw).to_string()); } if format_string.contains("%{mysql_caching_sha2}") { *format_string = format_string.replace( "%{mysql_caching_sha2}", &mysql_caching_sha2_pass(pw).to_string(), ); } if format_string.contains("%{postgres}") { *format_string = format_string.replace("%{postgres}", &postgres_pass(pw)); } if format_string.contains("%{username}") && c.username.is_some() { *format_string = format_string.replace("%{username}", c.username.as_ref().unwrap()); } if format_string.contains("%{database}") && c.database.is_some() { *format_string = format_string.replace("%{database}", c.database.as_ref().unwrap()); } if format_string.contains("%{mysql}") { *format_string = format_string.replace("%{mysql}", &mysql_pass(pw)); } if format_string.contains("%{servername}") && c.servername.is_some() { *format_string = format_string.replace("%{servername}", c.servername.as_ref().unwrap()); } if format_string.contains("%{totp}") && c.totp_key.is_some() { *format_string = format_string.replace("%{totp}", &totp_string(c)); } if format_string.contains("%{totpsecs}") && c.totp_key.is_some() { *format_string = format_string.replace("%{totpsecs}", &totp_secs_remaining(c).to_string()); } if format_string.contains("%{totpsecsmax}") && c.totp_key.is_some() { *format_string = format_string.replace("%{totpsecsmax}", &c.totp_step.unwrap().to_string()); } if format_string.contains("%{totpprogress}") && c.totp_key.is_some() { *format_string = format_string.replace("%{totpprogress}", &totp_progress(c)); } if format_string.contains("%{totpalgo}") && c.totp_key.is_some() { *format_string = format_string.replace( "%{totpalgo}", match c.totp_algo { TotpAlgo::Sha1 => "sha1", TotpAlgo::Sha256 => "sha256", TotpAlgo::Sha512 => "sha512", }, ); } *format_string = format_string.replace(r#"\n"#, "\n"); format_string.to_string() } pub fn create_pg_database(c: &Config) -> String { if c.create_database { let db = c.database.as_ref().unwrap_or(&"".to_string()).to_string(); let username = c.username.as_ref().unwrap_or(&"".to_string()).to_string(); return format!("create database {db}; alter database {db} owner to {username}; ") .to_string(); } "".to_string() } pub fn create_mysql_database(c: &Config) -> String { if c.create_database { return format!( "create database {}; ", c.database.as_ref().unwrap_or(&"".to_string()) ) .to_string(); } "".to_string() } pub fn create_nginx_vhost() -> String { let vhost = r#" server { listen 80; listen [::]:80; server_name %{servername} www.%{servername}; return 301 https://$host$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; server_name %{servername} www.%{servername}; access_log /var/log/nginx/%{servername}_access.log; error_log /var/log/nginx/%{servername}_error.log; include /etc/nginx/snippets/snakeoil.conf; location / { } } "#; vhost.to_string() } pub fn create_apache_vhost() -> String { let vhost = r#" ServerName %{servername} ServerAlias www.%{servername} DocumentRoot /home/%{username}/public_html ErrorLog ${APACHE_LOG_DIR}/%{servername}_error.log CustomLog ${APACHE_LOG_DIR}/%{servername}_access.log combined Require all granted AllowOverride All "#; vhost.to_string() } pub fn totp_string(c: &Config) -> String { fn to_bytes(n: u64) -> [u8; 8] { let mask: u64 = 0xff; let mut bytes: [u8; 8] = [0; 8]; (0..8).for_each(|i| bytes[7 - i] = (mask & (n >> (i * 8))) as u8); bytes } fn totp_maker(c: &Config, pw: &[u8]) -> String { let mut hash: Vec = vec![]; match c.totp_algo { TotpAlgo::Sha1 => { let mut mac = Hmac::::new_from_slice(pw).unwrap(); Hmac::::update(&mut mac, &to_bytes(c.totp_seconds() / c.totp_step.unwrap())); hash.extend_from_slice(&mac.finalize().into_bytes()); } TotpAlgo::Sha256 => { let mut mac = Hmac::::new_from_slice(pw).unwrap(); Hmac::::update( &mut mac, &to_bytes(c.totp_seconds() / c.totp_step.unwrap()), ); hash.extend_from_slice(&mac.finalize().into_bytes()); } TotpAlgo::Sha512 => { let mut mac = Hmac::::new_from_slice(pw).unwrap(); Hmac::::update( &mut mac, &to_bytes(c.totp_seconds() / c.totp_step.unwrap()), ); hash.extend_from_slice(&mac.finalize().into_bytes()); } } let offset: usize = (hash.last().unwrap() & 0xf) as usize; let binary: u64 = (((hash[offset] & 0x7f) as u64) << 24) | ((hash[offset + 1] as u64) << 16) | ((hash[offset + 2] as u64) << 8) | (hash[offset + 3] as u64); format!( "{:01$}", binary % (10_u64.pow(c.len.unwrap_or(6))), c.len.unwrap_or(6) as usize ) } let s = c .totp_key .as_ref() .unwrap() .trim() .to_lowercase() .to_string(); let password: &[u8] = &match base32::decode(Alphabet::Rfc4648Lower { padding: false }, &s) { Some(x) => x, None => { eprintln!("Cannot base32 convert {}", &s); std::process::exit(1); } }; totp_maker(c, password) } pub fn totp_secs_remaining(c: &Config) -> u64 { c.totp_step.unwrap() - c.totp_seconds() % c.totp_step.unwrap() } pub fn totp_progress_bar(c: &Config) -> String { let width: usize = 30; let percent: usize = width / (c.totp_step.unwrap() as usize); let remaining: usize = (c.totp_step.unwrap() as usize) - (c.totp_seconds() as usize) % (c.totp_step.unwrap() as usize); let progress = percent * remaining; let padding = width - progress; format!( "{empty:# String { let mut ret = "".to_string(); ret.push_str(&urgent_colour(totp_secs_remaining(c) as i32)); ret.push_str(&totp_progress_bar(c)); ret.push_str(&normal_colour()); ret.to_string() } pub fn set_wordlist(c: &mut Config) { if c.word_list.is_some() { let word_list = c.word_list.as_ref().unwrap(); if c.words.is_none() { let f = File::open(word_list); if f.is_err() { eprintln!("Cannot open {}: {}", word_list, f.err().unwrap()); std::process::exit(1); } let f = f.unwrap(); let mut line = String::new(); let mut br = BufReader::new(f); let mut wv: Vec = vec![]; loop { line.clear(); let l = br.read_line(&mut line); match l { Ok(i) => { if i == 0 { break; } } Err(_) => { break; } } // skip words with apostrophes etc if line.find('\'').is_some() { continue; } let s = line.clone().trim().to_string(); if s.is_empty() { continue; } if !valid_word(&s, c.pw_type) { continue; } wv.push(s); } c.words = Some(wv); } } } #[allow(deprecated)] pub fn pw_loop(matches: &getopts::Matches, c: &Config) { let pw = (if c.password.is_some() { c.password.as_ref().unwrap().to_string() } else { (*prng_string(c)).to_string() }) .to_string(); let mut donefmtopt = false; if matches.opt_present("pgfmt") { let mut format_string = format!("%{{pgfmt}} {} -- # %{{password}}\n", create_pg_database(c)).to_string(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("mysqlfmt") { let mut format_string = format!( "{}%{{mysqlfmt}} -- # %{{password}}\n", create_mysql_database(c) ) .to_string(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("mysqluserfmt") { let mut format_string = format!( "{}%{{mysqluserfmt}} -- # %{{password}}\n", create_mysql_database(c) ) .to_string(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("htauthfmt") { let mut format_string = "# %{password}\n%{htauthfmt}\n".to_string(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("nginxfmt") { let mut format_string = create_nginx_vhost(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("apachefmt") { let mut format_string = create_apache_vhost(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("userfmt") { let mut format_string = "%{userfmt} # %{password}\n".to_string(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("usermodfmt") { let mut format_string = "%{usermodfmt} # %{password}\n".to_string(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("totpfmt") { let mut format_string = "%{totpfmt}\n".to_string(); format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); donefmtopt = true; } if matches.opt_present("format") { let mut format_string = matches.opt_str("format").unwrap().to_string(); if format_string.trim() == "useradd" { format_string = "%{userfmt} # %{password}\n".to_string(); } if format_string.trim() == "usermod" { format_string = "%{usermodfmt} # %{password}\n".to_string(); } if format_string.trim() == "mysql" { format_string = "%{mysqlfmt} -- %{password}\n".to_string(); } if format_string.trim() == "pg" { format_string = "%{pgfmt} -- %{password}\n".to_string(); } if format_string.trim() == "htauth" { format_string = "# %{password}\n%{htauthfmt}\n".to_string(); } format_string = process_format_string(&mut format_string, c, &pw); print!("{}", format_string); return; } if donefmtopt { return; } if matches.opt_present("md5") { println!("{} {}", pw, md5_crypt::hash(&pw).unwrap()); return; } if matches.opt_present("sha1") { println!("{} {}", pw, sha1_crypt::hash(&pw).unwrap()); return; } if matches.opt_present("sha256") { println!("{} {}", pw, sha256_crypt::hash(&pw).unwrap()); return; } if matches.opt_present("sha512") { println!("{} {}", pw, sha512_crypt::hash(&pw).unwrap()); return; } if matches.opt_present("bcrypt") { println!("{} {}", pw, bcrypt::hash(&pw).unwrap()); return; } if matches.opt_present("des") { println!("{} {}", pw, unix_crypt::hash(&pw).unwrap()); return; } for j in 1..8 { let pw = (if c.password.is_some() { c.password.as_ref().unwrap().to_string() } else { (*prng_string(c)).to_string() }) .to_string(); print!("{}{}", pw, if j != 8 { " " } else { "" },); if c.word_list.is_some() { break; } } println!(); } fn parse_totp_item(c: &mut Config, items: Vec<&str>) { match items[0].to_lowercase().as_str() { "key" | "totp" => { c.totp_key = Some(items[1].to_string()); } "name" | "username" => { c.username = Some(items[1].to_string()); } "step" => { c.totp_step = match items[1].parse::() { Ok(l) => Some(l.into()), Err(_) => { eprintln!("cannot convert {} to number", items[1]); std::process::exit(1); } } } "algo" => { c.totp_algo = match items[1].to_lowercase().as_str() { "sha1" => TotpAlgo::Sha1, "sha256" => TotpAlgo::Sha256, "sha512" => TotpAlgo::Sha512, _ => { eprintln!("cannot convert {} to a TOTP algorithm", items[1]); std::process::exit(1); } } } "digits" => { c.len = match items[1].parse::() { Ok(l) => Some(l), Err(_) => { eprintln!("cannot convert {} to number", items[1]); std::process::exit(1); } } } "seconds" => { c.totp_seconds = match items[1].parse::() { Ok(l) => Some(l.into()), Err(_) => { eprintln!("cannot convert {} to number", items[1]); std::process::exit(1); } } } _ => { eprintln!("key {} isn't valid", items[0]); std::process::exit(1); } } } pub fn main_loop(matches: &getopts::Matches, c: &Config) { for _ in 0..c.number { if c.totp_key.is_some() { // step=30,hash=sha1,secret=sock; ; let totp_arr: Vec<_> = c.totp_key.as_ref().unwrap().split(";").collect(); for i in totp_arr { let i = i.trim(); if i.is_empty() { continue; } let c: &mut Config = &mut c.clone(); c.totp_key = Some(i.to_string()); let parts: Vec<_> = i.split(",").collect(); for p in parts { let p = p.trim(); if p.is_empty() { continue; } let items: Vec<_> = p.splitn(2, "=").collect(); if items.len() < 2 { continue; } parse_totp_item(c, items); } pw_loop(matches, c); } continue; } pw_loop(matches, c); } } pub fn sleep(millis: u64) { let duration = Duration::from_millis(millis); thread::sleep(duration); } pub fn urgent_colour(remaining: i32) -> String { if remaining < 6 { "\x1b[0;31m".to_string() } else { "\x1b[0;32m".to_string() } } pub fn normal_colour() -> String { "\x1b[0m".to_string() } pwtool-0.9.3/src/main.rs000064400000000000000000000266321046102023000132530ustar 00000000000000use getopts::Options; use pwtool::*; use std::env; const VERSION: &str = env!("CARGO_PKG_VERSION"); fn print_usage(program: &str, opts: Options) { let brief = format!("Usage: {} [options]", program); print!("{}", opts.usage(&brief)); } fn set_config_options( matches: &getopts::Matches, c: &mut Config, args: Vec, opts: Options, ) { fn set_totp_algo(matches: &getopts::Matches, c: &mut Config) { if matches.opt_present("sha256") { c.totp_algo = TotpAlgo::Sha256; } if matches.opt_present("sha512") { c.totp_algo = TotpAlgo::Sha512; } } if matches.opt_present("version") { println!("pwtool {VERSION}"); std::process::exit(0); } if matches.opt_present("words") { c.word_list = Some("/usr/share/dict/words".to_string()); } if matches.opt_present("createdatabase") { c.create_database = true; } if matches.opt_present("loopdelay") { c.loopdelay = match matches .opt_str("loopdelay") .as_ref() .unwrap() .parse::() { Ok(x) => Some(x), Err(x) => { eprintln!( "{} is not parsable as a number: {}", matches.opt_str("loopdelay").unwrap(), x ); print_usage(&args[0], opts); std::process::exit(3); } } } if matches.opt_present("wordsfile") { let words = matches.opt_str("wordsfile").unwrap(); c.word_list = Some(words); } let mut found = false; for opt in [ "onlynumeric", "onlyalpha", "onlyuppercase", "onlylowercase", "onlyextended", ] { if matches.opt_present(opt) { if found { eprintln!("cannot have multiple --only* options"); std::process::exit(1); } found = true; } } if matches.opt_present("onlynumeric") { c.pw_type = Some(PwClass::Num as u32); } if matches.opt_present("onlyalpha") { c.pw_type = Some(PwClass::Alpha as u32); } if matches.opt_present("onlyuppercase") { c.pw_type = Some(PwClass::Upper as u32); } if matches.opt_present("onlylowercase") { c.pw_type = Some(PwClass::Lower as u32); } if matches.opt_present("onlyextended") { c.pw_type = Some(PwClass::Ext as u32); } if matches.opt_present("onlyextended") { c.pw_type = Some(PwClass::Ext as u32); } if matches.opt_present("help") { print_usage(&args[0], opts); std::process::exit(0); } if env::var("PASSWORD").is_ok() { c.password = Some(env::var("PASSWORD").unwrap()); } if matches.opt_present("password") { c.password = matches.opt_str("password"); } if matches.opt_present("username") { c.username = matches.opt_str("username"); } if matches.opt_present("database") { c.database = matches.opt_str("database"); } if matches.opt_present("servername") { c.servername = matches.opt_str("servername"); } if env::var("TOTP").is_ok() { c.totp_key = Some(env::var("TOTP").unwrap()); set_totp_algo(matches, c); } if matches.opt_present("totp") { c.totp_key = matches.opt_str("totp"); set_totp_algo(matches, c); } if matches.opt_present("totpstep") { c.totp_step = match matches.opt_str("totpstep").as_ref().unwrap().parse::() { Ok(x) => Some(x), Err(x) => { eprintln!( "{} is not parsable as a number: {}", matches.opt_str("totpstep").unwrap(), x ); print_usage(&args[0], opts); std::process::exit(3); } }; } if matches.opt_present("totpseconds") { c.totp_seconds = match matches .opt_str("totpseconds") .as_ref() .unwrap() .parse::() { Ok(x) => Some(x), Err(x) => { eprintln!( "{} is not parsable as a number: {}", matches.opt_str("totpseconds").unwrap(), x ); print_usage(&args[0], opts); std::process::exit(3); } }; } c.number = if matches.opt_present("number") { let n = matches.opt_str("number").unwrap(); match n.parse::() { Ok(l) => l, Err(_) => { eprintln!("cannot convert {} to number", n); std::process::exit(1); } } } else if c.totp_key.is_some() { 1 } else { 20 }; if matches.opt_present("numeric") { c.pw_type = Some(match c.pw_type { Some(s) => s | PwClass::Num as u32, None => { PwClass::Alpha as u32 | PwClass::Lower as u32 | PwClass::Upper as u32 | PwClass::Num as u32 } }); } if matches.opt_present("alpha") { c.pw_type = Some(match c.pw_type { Some(s) => s | PwClass::Alpha as u32, None => { PwClass::Alpha as u32 | PwClass::Lower as u32 | PwClass::Upper as u32 | PwClass::Num as u32 } }); } if matches.opt_present("extended") { c.pw_type = Some(match c.pw_type { Some(s) => s | PwClass::Ext as u32, None => { PwClass::Alpha as u32 | PwClass::Lower as u32 | PwClass::Upper as u32 | PwClass::Num as u32 | PwClass::Ext as u32 } }); } if matches.opt_present("uppercase") { c.pw_type = Some(match c.pw_type { Some(s) => s | PwClass::Upper as u32, None => { PwClass::Alpha as u32 | PwClass::Lower as u32 | PwClass::Upper as u32 | PwClass::Num as u32 } }); } if matches.opt_present("lowercase") { c.pw_type = Some(match c.pw_type { Some(s) => s | PwClass::Lower as u32, None => { PwClass::Alpha as u32 | PwClass::Lower as u32 | PwClass::Upper as u32 | PwClass::Num as u32 } }); } if matches.opt_present("help") { print_usage(&args[0], opts); std::process::exit(0); } c.len = if matches.opt_present("length") { let n = matches.opt_str("length").unwrap(); match n.parse::() { Ok(l) => Some(l), Err(_) => { eprintln!("cannot convert {} to number", n); std::process::exit(1); } } } else if c.word_list.is_some() { Some(3) } else if c.totp_key.is_some() { Some(6) } else { Some(10) }; c.spaces = !matches.opt_present("nospaces"); for digest in ["bcrypt", "md5", "des", "sha1", "sha256", "sha512"] { if matches.opt_present(digest) { c.digest = Some(digest.to_string()); } } } fn main() { let mut c = Config::new(); let args: Vec = env::args().collect(); let mut opts = Options::new(); opts.optflag("h", "help", "display help"); opts.optflag("", "alpha", "use alpha characters (default)"); opts.optflag("", "numeric", "use numeric characters (default)"); opts.optflag("", "extended", "use extended characters"); opts.optflag("", "lowercase", "use lowercase characters (default)"); opts.optflag("", "uppercase", "use uppercase characters (default)"); opts.optflag("", "onlyuppercase", "use uppercase characters"); opts.optflag("", "onlylowercase", "use lowercase characters"); opts.optflag("", "onlynumeric", "use numeric characters"); opts.optflag("", "onlyextended", "use extended characters"); opts.optflag("", "onlyalpha", "use alpha characters"); opts.optflag("", "md5", "use MD5"); opts.optflag("", "bcrypt", "use bcrypt"); opts.optflag("", "des", "use traditional unix crypt"); opts.optflag("", "sha1", "use SHA1"); opts.optflag("", "sha256", "use SHA256"); opts.optflag("", "sha512", "use SHA512"); opts.optopt("", "loopdelay", "print and loop delay", "SECONDS"); opts.optflag("", "mysqlfmt", "alias for --format \"%{mysqlfmt}\\n\""); opts.optflag( "", "mysqluserfmt", "alias for --format \"%{mysqluserfmt}\\n\"", ); opts.optflag("", "pgfmt", "alias for --format \"%{pgfmt}\\n\""); opts.optflag("", "userfmt", "alias for --format \"%{userfmt}\\n\""); opts.optflag("", "usermodfmt", "alias for --format \"%{usermodfmt}\\n\""); opts.optflag("", "htauthfmt", "alias for --format \"%{htauthfm}\\n\""); opts.optflag("", "nginxfmt", "alias for --format \"%{nginxfmt}\\n\""); opts.optflag("", "apachefmt", "alias for --format \"%{apachefmt}\\n\""); opts.optflag("", "totpfmt", "alias for --format \"%{totpfmt}\\n\""); opts.optopt("", "username", "for %{username} formatter", "STRING"); opts.optopt("", "database", "for %{database} formatter", "STRING"); opts.optopt("", "servername", "for %{servername} formatter", "STRING"); opts.optopt( "", "password", "use input and don't generate, PASSWORD can also be an environment variable", "STRING", ); opts.optflag( "", "createdatabase", "when using --pgfmt or --mysql*fmt, prefix with a create database statement", ); opts.optopt( "", "totp", "use TOTP key (base32), TOTP can also be an environment variable", "STRING", ); opts.optopt("", "totpstep", "use TOTP step (default 30)", "SECONDS"); opts.optopt( "", "totpseconds", "use SECONDS as time reference", "SECONDS", ); opts.optflag("v", "version", "display version"); opts.optopt("n", "number", "number of passwords", "NUMBER"); opts.optopt( "l", "length", "length of passwords (default is three with a wordlist)", "NUMBER", ); opts.optflag("w", "words", "use default wordlist"); opts.optflag("", "nospaces", "don't join words with spaces"); opts.optopt("", "wordsfile", "use wordsfile", "FILE"); opts.optopt( "", "format", "output using a string: %{VAL} where VAL is md5, bcrypt, des, sha1, sha256, sha512, mysql, postgres, database, password or username.\nmysqlfmt, pgfmt, userfmt, htauthfmt\ntotp for the totp digits, totpprogress for bar graph, totpsecs for remaining secs, totpalgo for the algorithm and totpsecsmax for step", "FORMAT", ); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { eprintln!("{}", f); std::process::exit(1); } }; set_config_options(&matches, &mut c, args, opts); set_wordlist(&mut c); loop { if c.loopdelay.is_some() { print!("\x1b[2J\x1b[H"); } main_loop(&matches, &c); if c.loopdelay.is_some() { sleep((c.loopdelay.unwrap() * 1000).into()); } else { break; } } } pwtool-0.9.3/tests/pw.rs000064400000000000000000000040111046102023000133130ustar 00000000000000#[cfg(test)] mod test { use pwtool::*; #[test] fn test_acceptable_lowercase() { let test = "abc"; let test_bad = "ABC"; let set = Some(PwClass::Lower as u32); assert_eq!(valid_word(&test, set), true); assert_eq!(valid_word(&test_bad, set), false); } #[test] fn test_acceptable_uppercase() { let test = "ABC"; let test_bad = "abc"; let set = Some(PwClass::Upper as u32); assert_eq!(valid_word(&test, set), true); assert_eq!(valid_word(&test_bad, set), false); } #[test] fn test_acceptable_numeric() { let test = "abc1"; let test_bad = "abcABC"; let set = Some(PwClass::Num as u32); assert_eq!(valid_word(&test, set), true); assert_eq!(valid_word(&test_bad, set), false); } #[test] fn test_acceptable_extended() { let test_bad = "abcABC"; let set = Some(PwClass::Ext as u32); for j in '!'..='/' { assert_eq!(valid_word(j.to_string().as_ref(), set), true); } assert_eq!(valid_word(&test_bad, set), false); } #[test] fn test_gen_salt() { use base64::prelude::*; let salt = gen_salt_str(40); let e = BASE64_STANDARD.decode(salt); assert!(e.is_ok()); } #[test] fn test_postgres_pass() { let pass = postgres_pass("foo"); assert!(pass.len() > 40); } #[test] fn test_mysql_pass() { let pass = mysql_pass("foo"); assert!(pass.len() > 40); } #[test] fn test_mysql_caching_sha2_pass() { let pw = "test"; let salt = "testsalttestsalttest"; let pass = mysql_caching_sha2_pass_salt(pw, salt, 5); assert_eq!("nbYFPOYL.EkFOvR2sEnGxVOOXoQS7GweOApblsrm5e/", pass); } #[test] fn test_totp() { let mut c = Config::new(); c.totp_key = Some("GOODTHINGS".to_string()); c.totp_seconds = Some(1753443985); assert_eq!("756804", totp_string(&c)); } }