diff options
author | Tad Fisher <tadfisher@gmail.com> | 2018-03-04 11:26:13 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-04 11:26:13 -0800 |
commit | f88e6a338cd976bfa4e14567c1f3024ddc58ab7d (patch) | |
tree | 4549da42122c877121c38b2956abf5fbd479c554 | |
parent | 0737f1f3e8ad8e31b7b5d7eb9b6a6a21eb3781e9 (diff) | |
parent | 948c2f04cbaf2f065b0c21b80de82f1f0c946e54 (diff) | |
download | pass-otp-f88e6a338cd976bfa4e14567c1f3024ddc58ab7d.tar.gz pass-otp-f88e6a338cd976bfa4e14567c1f3024ddc58ab7d.zip |
Merge pull request #56 from tadfisher/develop
Merge branch 'develop' into master
-rw-r--r-- | .travis.yml | 32 | ||||
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | README.md | 16 | ||||
-rwxr-xr-x | otp.bash | 129 | ||||
-rw-r--r-- | pass-otp.1 | 38 | ||||
-rw-r--r-- | test/Makefile | 1 | ||||
-rwxr-xr-x | test/append.t | 11 | ||||
-rwxr-xr-x | test/code.t | 10 | ||||
-rwxr-xr-x | test/insert.t | 18 |
9 files changed, 214 insertions, 46 deletions
diff --git a/.travis.yml b/.travis.yml index 4122f71..d548c3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,45 @@ dist: trusty +sudo: false language: bash +env: + - PASS_VERSION=1.7.1 + - PASS_VERSION=master + addons: apt: - sources: - - debian-sid packages: + - cabal-install - expect + - ghc - oathtool - - shellcheck before_script: - - wget https://git.zx2c4.com/password-store/snapshot/password-store-1.7.tar.xz - - tar -xvf password-store-1.7.tar.xz - - ln -s password-store-1.7/src/password-store.sh pass + - wget https://git.zx2c4.com/password-store/snapshot/password-store-$PASS_VERSION.tar.xz + - tar -xvf password-store-$PASS_VERSION.tar.xz + - ln -s password-store-$PASS_VERSION/src/password-store.sh pass + - if [ ! -f "${HOME}/.cabal/bin/shellcheck" ]; then cabal update; cabal install ShellCheck; fi + - ln -s "${HOME}/.cabal/bin/shellcheck" shellcheck - export PATH=$PATH:. - export TEST_OPTS="-v" install: true +before_cache: + - rm $HOME/.cabal/logs/build.log + +cache: + directories: + - $HOME/.cabal + script: - make lint - cd test && make all + +notifications: + email: + recipients: + - tadfisher@gmail.com + on_success: never + on_failure: always @@ -29,4 +29,7 @@ uninstall: lint: shellcheck -s bash $(PROG).bash -.PHONY: install uninstall lint +test: + $(MAKE) -C test + +.PHONY: install uninstall lint test @@ -113,8 +113,20 @@ sudo make install ### Arch Linux -`pass-otp` is available in the -[Arch User Repository](https://aur.archlinux.org/packages/pass-otp/). +`pass-otp` is available in the `[community]` repository: + +``` +pacman -S pass-otp +``` + +### macOS + +``` +brew install oath-toolkit +git clone https://github.com/tadfisher/pass-otp +cd pass-otp +make install PREFIX=/usr/local +``` ## Requirements @@ -18,6 +18,26 @@ OATH=$(which oathtool) +## source: https://gist.github.com/cdown/1163649 +urlencode() { + local l=${#1} + for (( i = 0 ; i < l ; i++ )); do + local c=${1:i:1} + case "$c" in + [a-zA-Z0-9.~_-]) printf "%c" "$c";; + ' ') printf + ;; + *) printf '%%%.2X' "'$c" + esac + done +} + +urldecode() { + # urldecode <string> + + local url_encoded="${1//+/ }" + printf '%b' "${url_encoded//%/\\x}" +} + # Parse a Key URI per: https://github.com/google/google-authenticator/wiki/Key-Uri-Format # Vars are consumed by caller # shellcheck disable=SC2034 @@ -34,12 +54,13 @@ otp_parse_uri() { otp_type=${BASH_REMATCH[1]} otp_label=${BASH_REMATCH[3]} - otp_accountname=${BASH_REMATCH[6]} - [[ -z $otp_accountname ]] && otp_accountname=${BASH_REMATCH[4]} || otp_issuer=${BASH_REMATCH[4]} + otp_accountname=$(urldecode "${BASH_REMATCH[6]}") + [[ -z $otp_accountname ]] && otp_accountname=$(urldecode "${BASH_REMATCH[4]}") || otp_issuer=$(urldecode "${BASH_REMATCH[4]}") [[ -z $otp_accountname ]] && die "Invalid key URI (missing accountname): $otp_uri" local p=${BASH_REMATCH[7]} - local IFS=\&; local params=(${p[@]}); unset IFS + local params + local IFS=\&; read -r -a params <<< "$p"; unset IFS pattern='^(.+)=(.+)$' for param in "${params[@]}"; do @@ -50,7 +71,7 @@ otp_parse_uri() { algorithm) otp_algorithm=${BASH_REMATCH[2]} ;; period) otp_period=${BASH_REMATCH[2]} ;; counter) otp_counter=${BASH_REMATCH[2]} ;; - issuer) otp_issuer=${BASH_REMATCH[2]} ;; + issuer) otp_issuer=$(urldecode "${BASH_REMATCH[2]}") ;; *) ;; esac fi @@ -82,6 +103,29 @@ otp_read_uri() { otp_parse_uri "$uri" } +otp_read_secret() { + local uri prompt="$1" echo="$2" issuer accountname + issuer="$(urlencode "$3")" + accountname="$(urlencode "$4")" + + if [[ -t 0 ]]; then + if [[ $echo -eq 0 ]]; then + read -r -p "Enter secret for $prompt: " -s secret || exit 1 + echo + read -r -p "Retype secret for $prompt: " -s secret_again || exit 1 + echo + [[ "$secret" == "$secret_again" ]] || die "Error: the entered secrets do not match." + else + read -r -p "Enter secret for $prompt: " -e secret + fi + else + read -r secret + fi + + uri="otpauth://totp/$issuer:$accountname?secret=$secret&issuer=$issuer" + otp_parse_uri "$uri" +} + otp_insert() { local path="$1" passfile="$2" contents="$3" message="$4" @@ -104,15 +148,30 @@ Usage: Generate an OTP code and optionally put it on the clipboard. If put on the clipboard, it will be cleared in $CLIP_TIME seconds. - $PROGRAM otp insert [--force,-f] [--echo,-e] [pass-name] - Prompt for and insert a new OTP key URI. If pass-name is not supplied, - use the URI label. Optionally, echo the input. Prompt before overwriting - existing password unless forced. This command accepts input from stdin. + $PROGRAM otp insert [--force,-f] [--echo,-e] + [[--secret, -s] [--issuer,-i issuer] [--account,-a account]] + [pass-name] + Prompt for and insert a new OTP key. + + If 'secret' is specified, prompt for the OTP secret, assuming SHA1 + algorithm, 30-second period, and 6 OTP digits; one of 'issuer' or + 'account' is also required. Otherwise, prompt for a key URI; if + 'pass-name' is not supplied, use the URI label. + + Optionally, echo the input. Prompt before overwriting existing URI + unless forced. This command accepts input from stdin. + + $PROGRAM otp append [--force,-f] [--echo,-e] + [[--secret, -s] [--issuer,-i issuer] [--account,-a account]] + pass-name + Appends an OTP key URI to an existing password file. + + If 'secret' is specified, prompt for the OTP secret, assuming SHA1 + algorithm, 30-second period, and 6 OTP digits; one of 'issuer' or + 'account' is also required. Otherwise, prompt for a key URI. - $PROGRAM otp append [--force,-f] [--echo,-e] pass-name - Appends an OTP key URI to an existing password file. Optionally, echo - the input. Prompt before overwriting an existing URI unless forced. This - command accepts input from stdin. + Optionally, echo the input. Prompt before overwriting an existing URI + unless forced. This command accepts input from stdin. $PROGRAM otp uri [--clip,-c] [--qrcode,-q] pass-name Display the key URI stored in pass-name. Optionally, put it on the @@ -127,17 +186,20 @@ _EOF } cmd_otp_insert() { - local opts force=0 echo=0 - opts="$($GETOPT -o fe -l force,echo -n "$PROGRAM" -- "$@")" + local opts force=0 echo=0 from_secret=0 + opts="$($GETOPT -o fesi:a: -l force,echo,secret,issuer:,account: -n "$PROGRAM" -- "$@")" local err=$? eval set -- "$opts" while true; do case $1 in -f|--force) force=1; shift ;; -e|--echo) echo=1; shift ;; + -s|--secret) from_secret=1; shift;; + -i|--issuer) issuer=$2; shift; shift;; + -a|--account) account=$2; shift; shift;; --) shift; break ;; esac done - [[ $err -ne 0 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] [--echo,-e] [pass-name]" + [[ $err -ne 0 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] [--echo,-e] [--secret, -s] [--issuer,-i issuer] [--account,-a account] [pass-name]" local prompt path uri if [[ $# -eq 1 ]]; then @@ -147,7 +209,12 @@ cmd_otp_insert() { prompt="this token" fi - otp_read_uri "$prompt" $echo + if [[ $from_secret -eq 1 ]]; then + ([[ -z "$issuer" ]] || [[ -z "$account" ]]) && die "Missing issuer or account" + otp_read_secret "$prompt" $echo "$issuer" "$account" + else + otp_read_uri "$prompt" $echo + fi if [[ -z "$path" ]]; then [[ -n "$otp_issuer" ]] && path+="$otp_issuer/" @@ -162,17 +229,20 @@ cmd_otp_insert() { } cmd_otp_append() { - local opts force=0 echo=0 - opts="$($GETOPT -o fe -l force,echo -n "$PROGRAM" -- "$@")" + local opts force=0 echo=0 from_secret=0 + opts="$($GETOPT -o fesi:a: -l force,echo,secret,issuer:account: -n "$PROGRAM" -- "$@")" local err=$? eval set -- "$opts" while true; do case $1 in -f|--force) force=1; shift ;; -e|--echo) echo=1; shift ;; + -s|--secret) from_secret=1; shift;; + -i|--issuer) issuer=$2; shift; shift;; + -a|--account) account=$2; shift; shift;; --) shift; break ;; esac done - [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND append [--force,-f] [--echo,-e] pass-name" + [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND append [--force,-f] [--echo,-e] [--secret, -s] [--issuer,-i issuer] [--account,-a account] pass-name" local prompt uri local path="${1%/}" @@ -189,7 +259,12 @@ cmd_otp_append() { [[ -n "$existing" ]] && yesno "An OTP secret already exists for $path. Overwrite it?" - otp_read_uri "$path" $echo + if [[ $from_secret -eq 1 ]]; then + ([[ -z "$issuer" ]] || [[ -z "$account" ]]) && die "Missing issuer or account" + otp_read_secret "$prompt" $echo "$issuer" "$account" + else + otp_read_uri "$prompt" $echo + fi local replaced if [[ -n "$existing" ]]; then @@ -229,7 +304,7 @@ cmd_otp_code() { local path="${1%/}" local passfile="$PREFIX/$path.gpg" check_sneaky_paths "$path" - [[ ! -f $passfile ]] && die "Passfile not found" + [[ ! -f $passfile ]] && die "$path: passfile not found." contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile") while read -r -a line; do @@ -243,7 +318,7 @@ cmd_otp_code() { case "$otp_type" in totp) cmd="$OATH -b --totp" - [[ -n "$otp_algorithm" ]] && cmd+="=$otp_algorithm" + [[ -n "$otp_algorithm" ]] && cmd+=$(echo "=${otp_algorithm}"|tr "[:upper:]" "[:lower:]") [[ -n "$otp_period" ]] && cmd+=" --time-step-size=$otp_period"s [[ -n "$otp_digits" ]] && cmd+=" --digits=$otp_digits" cmd+=" $otp_secret" @@ -255,9 +330,13 @@ cmd_otp_code() { [[ -n "$otp_digits" ]] && cmd+=" --digits=$otp_digits" cmd+=" $otp_secret" ;; + + *) + die "$path: OTP secret not found." + ;; esac - local out; out=$($cmd) || die "Failed to generate OTP code for $path" + local out; out=$($cmd) || die "$path: failed to generate OTP code." if [[ "$otp_type" == "hotp" ]]; then # Increment HOTP counter in-place @@ -304,9 +383,9 @@ cmd_otp_uri() { fi done <<< "$contents" - if [[ clip -eq 1 ]]; then + if [[ $clip -eq 1 ]]; then clip "$otp_uri" "OTP key URI for $path" - elif [[ qrcode -eq 1 ]]; then + elif [[ $qrcode -eq 1 ]]; then qrcode "$otp_uri" "OTP key URI for $path" else echo "$otp_uri" @@ -37,28 +37,46 @@ and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds. This command is alternatively named \fBshow\fP. .TP -\fBotp insert\fP [ \fI--force\fP, \fI-f\fP ] [ \fI--echo\fP, \fI-e\fP ] [ \fIpass-name\fP ] +\fBotp insert\fP [ \fI--force\fP, \fI-f\fP ] [ \fI--echo\fP, \fI-e\fP ] \ +[ [ \fI--secret\fP, \fI-s\fP ] [ \fI--issuer\fP, \fI-i\fP \fIissuer\fP ] \ +[ \fI--account\fP, \fI-a\fP \fIaccount\fP ] ] [ \fIpass-name\fP ] Prompt for and insert a new OTP secret into the password store at -\fIpass-name\fP. The secret must be formatted according to the Key Uri Format; -see the documentation at +\fIpass-name\fP. + +If \fI--secret\fP is specified, prompt for the \fIsecret\fP value, assuming SHA1 +algorithm, 30-second period, and 6 OTP digits. One or both of \fIissuer\fP and +\fIaccount\fP must also be specified. + +If \fI--secret\fP is not specified, prompt for a key URI; see the documentation at .UR https://\:github.\:com/\:google/\:google-authenticator/\:wiki/\:Key-Uri-Format -.UE . -The URI is consumed from stdin; specify \fI--echo\fP or \fI-e\fP to echo input +.UE +for the key URI specification. + +The secret is consumed from stdin; specify \fI--echo\fP or \fI-e\fP to echo input when running this command interactively. If \fIpass-name\fP is not specified, convert the \fIissuer:accountname\fP URI label to a path in the form of -\fIisser/accountname\fP. Prompt before overwriting an existing password, unless +\fIisser/accountname\fP. Prompt before overwriting an existing secret, unless \fI--force\fP or \fI-f\fP is specified. This command is alternatively named \fBadd\fP. .TP -\fBotp append\fP [ \fI--force\fP, \fI-f\fP ] [ \fI--echo\fP, \fI-e\fP ] \fIpass-name\fP +\fBotp append\fP [ \fI--force\fP, \fI-f\fP ] [ \fI--echo\fP, \fI-e\fP ] \ +[ [ \fI--secret\fP, \fI-s\fP ] [ \fI--issuer\fP, \fI-i\fP \fIissuer\fP ] \ +[ \fI--account\fP, \fI-a\fP \fIaccount\fP ] ] \fIpass-name\fP Append an OTP secret to the password stored in \fIpass-name\fP, preserving any -existing lines. The secret must be formatted according to the Key Uri Format; -see the documentation at +existing lines. + +If \fI--secret\fP is specified, prompt for the \fIsecret\fP value, assuming SHA1 +algorithm, 30-second period, and 6 OTP digits. One or both of \fIissuer\fP and +\fIaccount\fP must also be specified. + +If \fI--secret\fP is not specified, prompt for a key URI; see the documentation at .UR https://\:github.\:com/\:google/\:google-authenticator/\:wiki/\:Key-Uri-Format -.UE . +.UE +for the key URI specification. + The URI is consumed from stdin; specify \fI--echo\fP or \fI-e\fP to echo input when running this command interactively. Prompt before overwriting an existing secret, unless \fI--force\fP or \fI-f\fP is specified. diff --git a/test/Makefile b/test/Makefile index 39f1df2..45882e7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -47,6 +47,7 @@ clean-except-prove-cache: clean: clean-except-prove-cache $(RM) .prove + $(RM) gnupg/random_seed aggregate-results-and-cleanup: $(T) $(MAKE) aggregate-results diff --git a/test/append.t b/test/append.t index bba3c60..7f6d0af 100755 --- a/test/append.t +++ b/test/append.t @@ -14,6 +14,17 @@ test_expect_success 'Reads non-terminal input' ' [[ $("$PASS" otp uri passfile) == "$uri" ]] ' +test_expect_success 'Read secret non-terminal input' ' + existing="foo bar baz" + secret=JBSWY3DPEHPK3PXP + uri="otpauth://totp/Example:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + "$PASS" insert -e passfile <<< "$existing" && + "$PASS" otp append -s -i Example -a alice@google.com -e passfile <<< "$secret" && + [[ $("$PASS" otp uri passfile) == "$uri" ]] +' + test_expect_success 'Reads terminal input in noecho mode' ' existing="foo bar baz" uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" diff --git a/test/code.t b/test/code.t index 02bd086..39310fb 100755 --- a/test/code.t +++ b/test/code.t @@ -4,6 +4,12 @@ export test_description='Tests pass otp code generation' . ./setup.sh +test_expect_success 'Fails for missing secret' ' + test_pass_init && + "$PASS" insert passfile <<< "12345" + test_expect_code 1 pass otp passfile +' + test_expect_success 'Generates TOTP code' ' uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" @@ -31,14 +37,14 @@ test_expect_success 'HOTP counter increments and preserves multiline contents' ' read -r -d "" existing <<EOF foo bar baz zab rab oof -$uri1 +$uri baz bar foo EOF read -r -d "" expected <<EOF foo bar baz zab rab oof -$uri2 +$inc baz bar foo EOF diff --git a/test/insert.t b/test/insert.t index e0d2441..1c8ddb2 100755 --- a/test/insert.t +++ b/test/insert.t @@ -112,4 +112,22 @@ test_expect_success 'Force overwrites key URI' ' [[ $("$PASS" show passfile) == "$uri2" ]] ' +test_expect_success 'Insert passfile from secret with options(issuer, accountname)' ' + secret="JBSWY3DPEHPK3PXP" + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + "$PASS" otp insert -s -i Example -a alice@google.com passfile <<< "$secret" && + echo [[ $("$PASS" show passfile) == "$uri" ]] +' + +test_expect_success 'Insert from secret without passfile' ' + secret="JBSWY3DPEHPK3PXP" + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + "$PASS" otp insert -s -i Example -a alice@google.com <<< "$secret" && + echo [[ $("$PASS" show Example/alice@google.com) == "$uri" ]] +' + test_done |