diff options
-rw-r--r-- | README.md | 2 | ||||
-rwxr-xr-x | otp.bash | 122 | ||||
-rw-r--r-- | pass-otp.1 | 28 | ||||
-rwxr-xr-x | test/append.t | 125 | ||||
-rwxr-xr-x | test/code.t | 24 |
5 files changed, 260 insertions, 41 deletions
@@ -107,7 +107,7 @@ sudo make install ## Requirements -- `pass` 1.7.0 or later for extenstion support +- `pass` 1.7.0 or later for extension support - `oathtool` for generating 2FA codes - `qrencode` for generating QR code images @@ -62,18 +62,32 @@ otp_parse_uri() { [[ "$otp_type" == 'hotp' ]] && [[ ! "$otp_counter" =~ $pattern ]] && die "Invalid key URI (missing counter): $otp_uri" } +otp_read_uri() { + local uri prompt="$1" echo="$2" + + if [[ -t 0 ]]; then + if [[ $echo -eq 0 ]]; then + read -r -p "Enter otpauth:// URI for $prompt: " -s uri || exit 1 + echo + read -r -p "Retype otpauth:// URI for $prompt: " -s uri_again || exit 1 + echo + [[ "$uri" == "$uri_again" ]] || die "Error: the entered URIs do not match." + else + read -r -p "Enter otpauth:// URI for $prompt: " -e uri + fi + else + read -r uri + fi + + otp_parse_uri "$uri" +} + otp_insert() { - local path="${1%/}" - local passfile="$PREFIX/$path.gpg" - local force=$2 - local contents="$3" - local message="$4" + local path="$1" passfile="$2" contents="$3" message="$4" check_sneaky_paths "$path" set_git "$passfile" - [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?" - mkdir -p -v "$PREFIX/$(dirname "$path")" set_gpg_recipients "$(dirname "$path")" @@ -95,6 +109,11 @@ Usage: use the URI label. Optionally, echo the input. Prompt before overwriting existing password unless forced. This command accepts input from stdin. + $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. + $PROGRAM otp uri [--clip,-c] [--qrcode,-q] pass-name Display the key URI stored in pass-name. Optionally, put it on the clipboard, or display a QR code. @@ -118,31 +137,17 @@ cmd_otp_insert() { --) shift; break ;; esac done - [[ $err -ne 0 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] [pass-name]" + [[ $err -ne 0 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] [--echo,-e] [pass-name]" local prompt path uri if [[ $# -eq 1 ]]; then - path="$1" + path="${1%/}" prompt="$path" else prompt="this token" fi - if [[ -t 0 ]]; then - if [[ $echo -eq 0 ]]; then - read -r -p "Enter otpauth:// URI for $prompt: " -s uri || exit 1 - echo - read -r -p "Retype otpauth:// URI for $prompt: " -s uri_again || exit 1 - echo - [[ "$uri" == "$uri_again" ]] || die "Error: the entered URIs do not match." - else - read -r -p "Enter otpauth:// URI for $prompt: " -e uri - fi - else - read -r uri - fi - - otp_parse_uri "$uri" + otp_read_uri "$prompt" $echo if [[ -z "$path" ]]; then [[ -n "$otp_issuer" ]] && path+="$otp_issuer/" @@ -150,7 +155,61 @@ cmd_otp_insert() { yesno "Insert into $path?" fi - otp_insert "$path" $force "$otp_uri" "Add OTP secret for $path to store." + local passfile="$PREFIX/$path.gpg" + [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?" + + otp_insert "$path" "$passfile" "$otp_uri" "Add OTP secret for $path to store." +} + +cmd_otp_append() { + local opts force=0 echo=0 + opts="$($GETOPT -o fe -l force,echo -n "$PROGRAM" -- "$@")" + local err=$? + eval set -- "$opts" + while true; do case $1 in + -f|--force) force=1; shift ;; + -e|--echo) echo=1; shift ;; + --) shift; break ;; + esac done + + [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND append [--force,-f] [--echo,-e] pass-name" + + local prompt uri + local path="${1%/}" + local passfile="$PREFIX/$path.gpg" + + [[ -f $passfile ]] || die "Passfile not found" + + local existing contents="" + while IFS= read -r line; do + [[ -z "$existing" && "$line" == otpauth://* ]] && existing="$line" + [[ -n "$contents" ]] && contents+=$'\n' + contents+="$line" + done < <($GPG -d "${GPG_OPTS[@]}" "$passfile") + + [[ -n "$existing" ]] && yesno "An OTP secret already exists for $path. Overwrite it?" + + otp_read_uri "$path" $echo + + local replaced + if [[ -n "$existing" ]]; then + while IFS= read -r line; do + [[ "$line" == otpauth://* ]] && line="$otp_uri" + [[ -n "$replaced" ]] && replaced+=$'\n' + replaced+="$line" + done <<< "$contents" + else + replaced="$contents"$'\n'"$otp_uri" + fi + + local message + if [[ -n "$existing" ]]; then + message="Replace OTP secret for $path." + else + message="Append OTP secret for $path." + fi + + otp_insert "$path" "$passfile" "$replaced" "$message" } cmd_otp_code() { @@ -167,7 +226,7 @@ cmd_otp_code() { [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--clip,-c] pass-name" - local path="$1" + local path="${1%/}" local passfile="$PREFIX/$path.gpg" check_sneaky_paths "$path" [[ ! -f $passfile ]] && die "Passfile not found" @@ -202,8 +261,14 @@ cmd_otp_code() { if [[ "$otp_type" == "hotp" ]]; then # Increment HOTP counter in-place - local uri=${otp_uri/&counter=$otp_counter/&counter=$counter} - otp_insert "$path" 1 "$uri" "Increment HOTP counter for $path." + local line replaced uri=${otp_uri/&counter=$otp_counter/&counter=$counter} + while IFS= read -r line; do + [[ "$line" == otpauth://* ]] && line="$uri" + [[ -n "$replaced" ]] && replaced+=$'\n' + replaced+="$line" + done <<< "$contents" + + otp_insert "$path" "$passfile" "$replaced" "Increment HOTP counter for $path." fi if [[ $clip -ne 0 ]]; then @@ -255,6 +320,7 @@ cmd_otp_validate() { case "$1" in help|--help|-h) shift; cmd_otp_usage "$@" ;; insert|add) shift; cmd_otp_insert "$@" ;; + append) shift; cmd_otp_append "$@" ;; uri) shift; cmd_otp_uri "$@" ;; validate) shift; cmd_otp_validate "$@" ;; code|show) shift; cmd_otp_code "$@" ;; @@ -52,6 +52,18 @@ convert the \fIissuer:accountname\fP URI label to a path in the form of \fBadd\fP. .TP +\fBotp append\fP [ \fI--force\fP, \fI-f\fP ] [ \fI--echo\fP, \fI-e\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 +.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 +when running this command interactively. Prompt before overwriting an existing +secret, unless \fI--force\fP or \fI-f\fP is specified. + +.TP \fBotp uri\fP [ \fI--clip\fP, \fI-c\fP | \fI--qrcode\fP, \fI-q\fP ] \fIpass-name\fP Print the key URI stored in \fIpass-name\fP to stdout. If \fI--clip\fP or @@ -75,20 +87,13 @@ information about this format, see the documentation at .SH OPTIONS .TP -\fB\-c\fP, \fB--clip\fP -Put the OTP code in the clipboard. - -.TP -\fB\-f\fP, \fB--force\fP -Force update and do not wait for user instruction. - -.TP -\fBhelp\fP, \fB\-h\fB, \-\-help\fR +\fBhelp\fP, \fB\-h\fP, \fB\-\-help\fP Show usage message. .SH SEE ALSO -.BR pass(1), - +.BR pass (1), +.BR qrencode (1), +.BR zbarimg (1) .SH AUTHORS .B pass-otp @@ -97,7 +102,6 @@ was written by Tad Fisher .ME . - .SH COPYING This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/test/append.t b/test/append.t new file mode 100755 index 0000000..bba3c60 --- /dev/null +++ b/test/append.t @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +export test_description="Tests pass otp append commands" + +. ./setup.sh + +test_expect_success 'Reads non-terminal input' ' + existing="foo bar baz" + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Foo" + + test_pass_init && + "$PASS" insert -e passfile <<< "$existing" && + "$PASS" otp append -e passfile <<< "$uri" && + [[ $("$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" + + test_pass_init && + "$PASS" insert -e passfile <<< "$existing" && + { expect -d <<EOD + spawn "$PASS" otp append passfile + expect { + "Enter" { + send "$uri\r" + exp_continue + } + "Retype" { + send "$uri\r" + exp_continue + } + eof + } +EOD + } && + [[ $("$PASS" otp uri passfile) == "$uri" ]] +' + +test_expect_success 'Reads terminal input in echo mode' ' + existing="foo bar baz" + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + "$PASS" insert -e passfile <<< "$existing" && + { + expect <<EOD + spawn "$PASS" otp append -e passfile + expect { + "Enter" { + send "$uri\r" + exp_continue + } + eof + } +EOD + } && + [[ $("$PASS" otp uri passfile) == "$uri" ]] +' + +test_expect_success 'Prompts before overwriting key URI' ' + existing="foo bar baz" + uri1="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Foo" + uri2="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Bar" + + test_pass_init && + "$PASS" insert -e passfile <<< "$existing" && + "$PASS" otp append -e passfile <<< "$uri1" && + { + expect -d <<EOD + spawn "$PASS" otp append -e passfile + expect { + "Enter" { + send "$uri2\r" + exp_continue + } + "An OTP secret already exists" { + send "n\r" + exp_continue + } + eof + } +EOD + } && + [[ $("$PASS" otp uri passfile) == "$uri1" ]] +' + +test_expect_success 'Force overwrites key URI' ' + existing="foo bar baz" + uri1="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Foo" + uri2="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Bar" + + test_pass_init && + "$PASS" insert -e passfile <<< "$existing" && + "$PASS" otp append -e passfile <<< "$uri1" && + "$PASS" otp append -ef passfile <<< "$uri2" && + [[ $("$PASS" otp uri passfile) == "$uri2" ]] +' + +test_expect_success 'Preserves multiline contents' ' + uri1="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Foo" + uri2="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Bar" + + read -r -d "" existing <<EOF +foo bar baz +zab rab oof +$uri1 +baz bar foo +EOF + + read -r -d "" expected <<EOF +foo bar baz +zab rab oof +$uri2 +baz bar foo +EOF + + test_pass_init && + "$PASS" insert -mf passfile <<< "$existing" && + "$PASS" otp append -ef passfile <<< "$uri2" && + [[ $("$PASS" show passfile) == "$expected" ]] +' + +test_done diff --git a/test/code.t b/test/code.t index 4faea03..02bd086 100755 --- a/test/code.t +++ b/test/code.t @@ -24,4 +24,28 @@ test_expect_success 'Generates HOTP code and increments counter' ' [[ $("$PASS" otp uri passfile) == "$inc" ]] ' +test_expect_success 'HOTP counter increments and preserves multiline contents' ' + uri="otpauth://hotp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&counter=10&issuer=Example" + inc="otpauth://hotp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&counter=11&issuer=Example" + + read -r -d "" existing <<EOF +foo bar baz +zab rab oof +$uri1 +baz bar foo +EOF + + read -r -d "" expected <<EOF +foo bar baz +zab rab oof +$uri2 +baz bar foo +EOF + + test_pass_init && + "$PASS" insert -mf passfile <<< "$existing" && + "$PASS" otp code passfile && + [[ $("$PASS" show passfile) == "$expected" ]] +' + test_done |