From 01a95a45932f9a54c2c2fc3423824bdbac3918d8 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sat, 18 Mar 2017 15:37:07 -0700 Subject: Parse key URIs --- otp.bash | 44 ++++++++++++++++++++++++++++++++++++++++++++ test/uri.t | 27 +++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100755 test/uri.t diff --git a/otp.bash b/otp.bash index 4a21322..93f815e 100755 --- a/otp.bash +++ b/otp.bash @@ -18,6 +18,45 @@ OATH=$(which oathtool) +# Parse a Key URI per: https://github.com/google/google-authenticator/wiki/Key-Uri-Format +# Vars are consumed by caller +# shellcheck disable=SC2034 +otp_parse_uri() { + local uri="$*" + + uri="${uri//\`/%60}" + uri="${uri//\"/%22}" + + local pattern='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?))?(\?([^#&?]+))(&([^#&?]+))*$' + [[ "$uri" =~ $pattern ]] || die "Cannot parse OTP key URI: $uri" + + otp_uri=${BASH_REMATCH[0]} + 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]} + + local parameters=(${BASH_REMATCH[@]:7}) + pattern='^([^?&=]+)(=(.+))$' + for param in "${parameters[@]}"; do + if [[ "$param" =~ $pattern ]]; then + case ${BASH_REMATCH[1]} in + secret) otp_secret=${BASH_REMATCH[3]} ;; + digits) otp_digits=${BASH_REMATCH[3]} ;; + algorithm) otp_algorithm=${BASH_REMATCH[3]} ;; + period) otp_period=${BASH_REMATCH[3]} ;; + counter) otp_counter=${BASH_REMATCH[3]} ;; + issuer) otp_issuer=${BASH_REMATCH[3]} ;; + *) ;; + esac + fi + done + + [[ -z "$otp_secret" ]] && die "Invalid key URI (missing secret): $otp_uri" + [[ "$otp_type" == 'hotp' && -z "$otp_counter" ]] && die "Invalid key URI (missing counter): $otp_uri" +} + otp_increment_counter() { local ret=$1 local counter=$2 contents="$3" path="$4" passfile="$5" @@ -265,11 +304,16 @@ cmd_otp_uri() { fi } +cmd_otp_validate() { + otp_parse_uri "$1" +} + case "$1" in help|--help|-h) shift; cmd_otp_usage "$@" ;; show) shift; cmd_otp_show "$@" ;; insert|add) shift; cmd_otp_insert "$@" ;; uri) shift; cmd_otp_uri "$@" ;; + validate) shift; cmd_otp_validate "$@" ;; *) cmd_otp_show "$@" ;; esac exit 0 diff --git a/test/uri.t b/test/uri.t new file mode 100755 index 0000000..9fbe66c --- /dev/null +++ b/test/uri.t @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +export test_description='Tests pass otp URI parsing' + +. ./setup.sh + +test_expect_success 'Parses a basic TOTP URI' ' + "$PASS" otp validate "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" +' + +test_expect_success 'Parses a complex TOTP URI' ' + "$PASS" otp validate otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 +' + +test_expect_success 'Fails for bogus URL' ' + test_must_fail "$PASS" otp validate https://www.google.com/ +' + +test_expect_success 'Fails for missing secret' ' + test_must_fail "$PASS" otp validate otpauth://totp/ACME%20Co:john.doe@email.com?issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 +' + +test_expect_success 'Fails for missing counter' ' + test_must_fail "$PASS" otp validate otpauth://hotp?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ +' + +test_done -- cgit v1.2.3 From 72373e86cea699c5fb69cfff436e390186cc9e4e Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sat, 18 Mar 2017 18:20:28 -0700 Subject: Add cmd_insert_uri; refactor tests --- otp.bash | 28 ++++++++++++++++++++++++---- test/insert.t | 40 ++++++++++++++++++++++++++++++++++------ test/setup.sh | 16 ++++++++++------ 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/otp.bash b/otp.bash index 93f815e..1d12c8b 100755 --- a/otp.bash +++ b/otp.bash @@ -22,7 +22,7 @@ OATH=$(which oathtool) # Vars are consumed by caller # shellcheck disable=SC2034 otp_parse_uri() { - local uri="$*" + local uri="$1" uri="${uri//\`/%60}" uri="${uri//\"/%22}" @@ -75,17 +75,18 @@ otp_increment_counter() { } otp_insert() { + echo "args: $*" + local path="${1%/}" local passfile="$PREFIX/$path.gpg" local force=$2 local contents="$3" check_sneaky_paths "$path" + set_git "$passfile" [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?" - set_git "$passfile" - mkdir -p -v "$PREFIX/$(dirname "$path")" set_gpg_recipients "$(dirname "$path")" @@ -94,6 +95,25 @@ otp_insert() { git_add_file "$passfile" "Add given OTP secret for $path to store." } +otp_insert_uri() { + local opts force=0 + opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")" + local err=$? + eval set -- "$opts" + while true; do case $1 in + -f|--force) force=1; shift ;; + --) shift; break ;; + esac done + + [[ $err -ne 0 || $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] uri pass-name" + + local uri="$1" + + otp_parse_uri "$uri" + + otp_insert "$2" $force "$otp_uri" +} + otp_insert_totp() { local opts contents secret="" algorithm="sha1" period=30 digits=6 force=0 opts="$($GETOPT -o s:a:p:d:f -l secret:,algorithm:,period:,digits:,force -n "$PROGRAM" -- "$@")" @@ -203,7 +223,7 @@ cmd_otp_insert() { case "$1" in totp) shift; otp_insert_totp "$@" ;; hotp) shift; otp_insert_hotp "$@" ;; - *) die "Invalid OTP type '$1'. May be one of 'totp' or 'hotp'" ;; + *) otp_insert_uri "$@" ;; esac } diff --git a/test/insert.t b/test/insert.t index a78971f..e04914e 100755 --- a/test/insert.t +++ b/test/insert.t @@ -4,16 +4,44 @@ export test_description="Tests pass otp insert commands" . ./setup.sh +test_expect_success 'Inserts a key URI' ' + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + "$PASS" otp insert "$uri" passfile && + [[ $("$PASS" show passfile) == "$uri" ]] +' + +test_expect_success 'Prompts before overwriting key URI' ' + 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" otp insert "$uri1" passfile && + test_faketty "echo n | $PASS otp insert $uri2 passfile" && + [[ $("$PASS" show passfile) == "$uri1" ]] +' + +test_expect_success 'Force overwrites key URI' ' + 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" otp insert "$uri1" passfile && + "$PASS" otp insert -f "$uri2" passfile && + [[ $("$PASS" show passfile) == "$uri2" ]] +' + test_expect_success 'Inserts a basic TOTP key' ' - "$PASS" init $KEY1 && - "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA totp-secret + test_pass_init && + "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA passfile ' test_expect_success 'Commits insert to git' ' - git init "$PASSWORD_STORE_DIR" && - "$PASS" init $KEY1 && - "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA totp-secret2 && - git log --no-decorate -1 | grep "Add given OTP secret for totp-secret2 to store." + test_pass_init && + pass git init && + "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA passfile && + git log --no-decorate -1 | grep "Add given OTP secret for passfile to store." ' test_done diff --git a/test/setup.sh b/test/setup.sh index f896382..b363e6d 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -33,12 +33,6 @@ export PASSWORD_STORE_ENABLE_EXTENSIONS=true export PASSWORD_STORE_EXTENSIONS_DIR="$EXT_HOME" export PASSWORD_STORE_DIR="$SHARNESS_TRASH_DIRECTORY/test-store" -rm -rf "$PASSWORD_STORE_DIR" -mkdir -p "$PASSWORD_STORE_DIR" -if [[ ! -d $PASSWORD_STORE_DIR ]]; then - echo "Could not create $PASSWORD_STORE_DIR" - exit 1 -fi export GIT_DIR="$PASSWORD_STORE_DIR/.git" export GIT_WORK_TREE="$PASSWORD_STORE_DIR" @@ -65,3 +59,13 @@ KEY2="D774A374" # pass test key 2 KEY3="EB7D54A8" # pass test key 3 KEY4="E4691410" # pass test key 4 KEY5="39E5020C" # pass test key 5 + +# Test helpers +test_pass_init() { + rm -rf "$PASSWORD_STORE_DIR" + "$PASS" init "$KEY1" +} + +test_faketty() { + script -qfc "$(printf "%q " "$@")" +} -- cgit v1.2.3 From 8367284176c086d3bf203923bbd0abd9c0786e33 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sat, 18 Mar 2017 23:56:24 -0700 Subject: Fix overwrite prompt test --- test/insert.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/insert.t b/test/insert.t index e04914e..dbd6e2a 100755 --- a/test/insert.t +++ b/test/insert.t @@ -18,7 +18,7 @@ test_expect_success 'Prompts before overwriting key URI' ' test_pass_init && "$PASS" otp insert "$uri1" passfile && - test_faketty "echo n | $PASS otp insert $uri2 passfile" && + test_faketty "$PASS" otp insert "$uri2" passfile < <(echo n) && [[ $("$PASS" show passfile) == "$uri1" ]] ' -- cgit v1.2.3 From 3aeea0b8ab9c7bbbfdf403fe5e0908a1160ef08b Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 16:23:26 -0700 Subject: Replace cmd_insert_$type with cmd_insert_spec; insert key uris --- otp.bash | 465 +++++++++++++++++++++++++++++++--------------------------- test/insert.t | 28 +++- 2 files changed, 272 insertions(+), 221 deletions(-) diff --git a/otp.bash b/otp.bash index 1d12c8b..dad46b8 100755 --- a/otp.bash +++ b/otp.bash @@ -18,6 +18,17 @@ OATH=$(which oathtool) +otp_urlencode() { + local LANG=C + for ((i=0; i<${#1}; i++)); do + if [[ ${1:$i:1} =~ ^[a-zA-Z0-9\.\~_-]$ ]]; then + printf "%s" "${1:$i:1}" + else + printf '%%%02X' "'${1:$i:1}" + fi + done +} + # Parse a Key URI per: https://github.com/google/google-authenticator/wiki/Key-Uri-Format # Vars are consumed by caller # shellcheck disable=SC2034 @@ -57,42 +68,83 @@ otp_parse_uri() { [[ "$otp_type" == 'hotp' && -z "$otp_counter" ]] && die "Invalid key URI (missing counter): $otp_uri" } +otp_build_uri() { + local type="$1" issuer="$2" accountname="$3" secret="$4" algorithm="$5" \ + digits="$6" period="$7" counter="$8" + + local uri="otpauth://$type/" + + local pattern='^[^:]+$' + if [[ -n "$issuer" ]]; then + [[ "$issuer" =~ $pattern ]] || die "Invalid character in issuer: ':'" + issuer=$(otp_urlencode "$issuer") + fi + + [[ -z "$accountname" ]] && die "Missing accountname" + [[ "$accountname" =~ $pattern ]] || die "Invalid character in accountname: ':'" + accountname=$(otp_urlencode "$accountname") + + if [[ -n "$issuer" ]]; then + uri+="$issuer:$accountname" + else + uri+="$accountname" + fi + + [[ -z "$secret" ]] && die "Missing secret"; uri+="?secret=$secret" + [[ -n "$algorithm" ]] && uri+="&algorithm=$algorithm" + + case "$1" in + totp) + [[ -n "$digits" ]] && uri+="&digits=$digits" + [[ -n "$period" ]] && uri+="&period=$period" + ;; + + hotp) + [[ -z "$counter" ]] && die "Missing counter"; uri+="&counter=$counter" + ;; + + *) die "Invalid OTP type '$1'" ;; + esac + + [[ -n "$issuer" ]] && uri+="&issuer=$issuer" + + echo "$uri" +} + otp_increment_counter() { - local ret=$1 - local counter=$2 contents="$3" path="$4" passfile="$5" + local ret=$1 + local counter=$2 contents="$3" path="$4" passfile="$5" - local inc=$((counter+1)) + local inc=$((counter+1)) - contents=${contents//otp_counter: $counter/otp_counter: $inc} + contents=${contents//otp_counter: $counter/otp_counter: $inc} - set_gpg_recipients "$(dirname "$path")" + set_gpg_recipients "$(dirname "$path")" - $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted." + $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted." - git_add_file "$passfile" "Update HOTP counter value for $path." + git_add_file "$passfile" "Update HOTP counter value for $path." - eval "$ret='$inc'" + eval "$ret='$inc'" } otp_insert() { - echo "args: $*" - - local path="${1%/}" - local passfile="$PREFIX/$path.gpg" - local force=$2 - local contents="$3" + local path="${1%/}" + local passfile="$PREFIX/$path.gpg" + local force=$2 + local contents="$3" - check_sneaky_paths "$path" - set_git "$passfile" + check_sneaky_paths "$path" + set_git "$passfile" - [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?" + [[ $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")" + mkdir -p -v "$PREFIX/$(dirname "$path")" + set_gpg_recipients "$(dirname "$path")" - $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted." + $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted." - git_add_file "$passfile" "Add given OTP secret for $path to store." + git_add_file "$passfile" "Add given OTP secret for $path to store." } otp_insert_uri() { @@ -114,214 +166,187 @@ otp_insert_uri() { otp_insert "$2" $force "$otp_uri" } -otp_insert_totp() { - local opts contents secret="" algorithm="sha1" period=30 digits=6 force=0 - opts="$($GETOPT -o s:a:p:d:f -l secret:,algorithm:,period:,digits:,force -n "$PROGRAM" -- "$@")" - local err=$? - eval set -- "$opts" - while true; do case $1 in - -s|--secret) secret="$2"; shift 2 ;; - -a|--algorithm) algorithm="$2"; shift 2 ;; - -p|--period) period="$2"; shift 2 ;; - -d|--digits) digits="$2"; shift 2 ;; - -f|--force) force=1; shift ;; - --) shift; break ;; - esac done - - [[ $err -ne 0 && $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND insert totp [--secret=key,s key] [--algorithm=algorithm,-a algorithm] [--period=seconds,-p seconds] [--digits=digits,-d digits] [--force,-f] pass-name" - - case $algorithm in - sha1|sha256|sha512) ;; - *) die "Invalid algorithm '$algorithm'. May be one of 'sha1', 'sha256', or 'sha512'" ;; - esac - - case $digits in - 6|8) ;; - *) die "Invalid digits '$digits'. May be one of '6' or '8'" ;; - esac - - if [[ -z $secret ]]; then - read -r -p "Enter secret (base32-encoded): " -s secret || exit 1 - fi - - contents=$(cat <<-_EOF - otp_secret: $secret - otp_type: totp - otp_algorithm: $algorithm - otp_period: $period - otp_digits: $digits - _EOF - ) - - otp_insert "$1" $force "$contents" -} +otp_insert_spec() { + local opts contents secret issuer accountname algorithm period digits counter force=0 + local type="$1"; shift -otp_insert_hotp() { - local opts contents secret="" digits=6 force=0 - opts="$($GETOPT -o s:d:f -l secret:,digits:,force -n "$PROGRAM" -- "$@")" - local err=$? - eval set -- "$opts" - while true; do case $1 in - -s|--secret) secret="$2"; shift 2 ;; - -a|--algorithm) algorithm="$2"; shift 2 ;; - -d|--digits) digits="$2"; shift 2 ;; - -f|--force) force=1; shift ;; - --) shift; break ;; - esac done - - [[ $err -ne 0 || $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND insert hotp [--secret=key,s key] [--digits=digits,-d digits] [--force,-f] pass-name counter" - - case $digits in - 6|8) ;; - *) die "Invalid digits '$digits'. May be one of '6' or '8'" ;; - esac - - if [[ -z $secret ]]; then - read -r -p "Enter secret (base32-encoded): " -s secret || exit 1 - fi - - local counter="$2" - [[ $counter =~ ^[0-9]+$ ]] || die "Invalid counter '$counter'. Must be a positive number" - - contents=$(cat <<-_EOF - otp_secret: $secret - otp_type: hotp - otp_counter: $counter - otp_digits: $digits - _EOF - ) - - otp_insert "$1" $force "$contents" + opts="$($GETOPT -o s:i:n:a:p:d:f -l secret:,issuer:,accountname:,algorithm:,period:,digits:,force -n "$PROGRAM" -- "$@")" + local err=$? + eval set -- "$opts" + while true; do case "$1" in + -s|--secret) secret="$2"; shift 2 ;; + -i|--issuer) issuer="$2"; shift 2 ;; + -n|--accountname) accountname="$2"; shift 2 ;; + -a|--algorithm) algorithm="$2"; shift 2 ;; + -p|--period) period="$2"; shift 2 ;; + -d|--digits) digits="$2"; shift 2 ;; + -f|--force) force=1; shift ;; + --) shift; break ;; + esac done + + [[ $type == "totp" && ($err -ne 0 || $# -ne 1) ]] && + die "Usage: $PROGRAM $COMMAND insert totp [--secret=key,s key] [--algorithm=algorithm,-a algorithm] [--period=seconds,-p seconds] [--digits=digits,-d digits] [--force,-f] pass-name" + + [[ $type == "hotp" && ($err -ne 0 || $# -ne 2) ]] && + die "Usage: $PROGRAM $COMMAND insert hotp [--secret=key,s key] [--digits=digits,-d digits] [--force,-f] pass-name counter" + + local path="$1" counter="$2" + + [[ -n "$algorithm" ]] && case $algorithm in + sha1|sha256|sha512) ;; + *) die "Invalid algorithm '$algorithm'. May be one of 'sha1', 'sha256', or 'sha512'" ;; + esac + + [[ -n "$digits" ]] && case $digits in + 6|8) ;; + *) die "Invalid digits '$digits'. May be one of '6' or '8'" ;; + esac + + if [[ -z $secret ]]; then + read -r -p "Enter secret (base32-encoded): " -s secret || die "Missing secret" + fi + + # Populate issuer and accountname from either options or path + if [[ -z $accountname ]]; then + accountname="$(basename "$path")" + if [[ -z "$issuer" ]]; then + issuer="$(basename "$(dirname "$path")")" + [[ "$issuer" == "." ]] && unset issuer + fi + fi + + local uri; uri=$(otp_build_uri "$type" "$issuer" "$accountname" "$secret" "$algorithm" "$period" "$digits" "$counter") + + otp_insert "$1" $force "$uri" } cmd_otp_usage() { - cat <<-_EOF - Usage: - $PROGRAM otp [show] [--clip,-c] pass-name - 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 totp [--secret=key,-s key] [--algorithm alg,-a alg] - [--period=seconds,-p seconds] - [--digits=digits,-d digits] [--force,-f] pass-name - Insert new TOTP secret. Prompt before overwriting existing password - unless forced. - $PROGRAM otp insert hotp [--secret=secret,-s secret] - [--digits=digits,-d digits] [--force,-f] - pass-name counter - Insert new HOTP secret with initial counter. Prompt before overwriting - existing password unless forced. - $PROGRAM otp uri [--clip,-c] [--qrcode,-q] pass-name - Create a secret key URI suitable for importing into other TOTP clients. - Optionally, put it on the clipboard, or display a QR code. - - More information may be found in the pass-otp(1) man page. - _EOF - exit 0 + cat <<-_EOF +Usage: + $PROGRAM otp [show] [--clip,-c] pass-name + 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 totp [--secret=key,-s key] [--algorithm alg,-a alg] + [--period=seconds,-p seconds] + [--digits=digits,-d digits] [--force,-f] pass-name + Insert new TOTP secret. Prompt before overwriting existing password + unless forced. + $PROGRAM otp insert hotp [--secret=secret,-s secret] + [--digits=digits,-d digits] [--force,-f] + pass-name counter + Insert new HOTP secret with initial counter. Prompt before overwriting + existing password unless forced. + $PROGRAM otp uri [--clip,-c] [--qrcode,-q] pass-name + Create a secret key URI suitable for importing into other TOTP clients. + Optionally, put it on the clipboard, or display a QR code. + +More information may be found in the pass-otp(1) man page. +_EOF + exit 0 } cmd_otp_insert() { - case "$1" in - totp) shift; otp_insert_totp "$@" ;; - hotp) shift; otp_insert_hotp "$@" ;; - *) otp_insert_uri "$@" ;; - esac + case "$1" in + totp|hotp) otp_insert_spec "$@" ;; + *) otp_insert_uri "$@" ;; + esac } cmd_otp_show() { local opts contents clip=0 secret="" type="" algorithm="" counter="" period=30 digits=6 - opts="$($GETOPT -o c -l clip -n "$PROGRAM" -- "$@")" - local err=$? - eval set -- "$opts" - while true; do case $1 in - -c|--clip) clip=1; shift ;; - --) shift; break ;; - esac done - - [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND show [--clip,-c] pass-name" - - local path="$1" - local passfile="$PREFIX/$path.gpg" - check_sneaky_paths "$path" - [[ ! -f $passfile ]] && die "Passfile not found" - - contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile") - while read -r -a line; do case ${line[0]} in - otp_secret:) secret=${line[1]} ;; - otp_type:) type=${line[1]} ;; - otp_algorithm:) algorithm=${line[1]} ;; - otp_period:) period=${line[1]} ;; - otp_counter:) counter=${line[1]} ;; - otp_digits:) digits=${line[1]} ;; - *) true ;; - esac done <<< "$contents" - - [[ -z $secret ]] && die "Missing otp_secret: line in $passfile" - [[ -z $type ]] && die "Missing otp_type: line in $passfile" - [[ $type = "totp" && -z $algorithm ]] && die "Missing otp_algorithm: line in $passfile" - [[ $type = "hotp" && -z $counter ]] && die "Missing otp_counter: line in $passfile" - - local out - case $type in - totp) out=$($OATH -b --totp="$algorithm" --time-step-size="$period"s --digits="$digits" "$secret") ;; - hotp) otp_increment_counter counter "$counter" "$contents" "$path" "$passfile" > /dev/null \ + opts="$($GETOPT -o c -l clip -n "$PROGRAM" -- "$@")" + local err=$? + eval set -- "$opts" + while true; do case $1 in + -c|--clip) clip=1; shift ;; + --) shift; break ;; + esac done + + [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND show [--clip,-c] pass-name" + + local path="$1" + local passfile="$PREFIX/$path.gpg" + check_sneaky_paths "$path" + [[ ! -f $passfile ]] && die "Passfile not found" + + contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile") + while read -r -a line; do case ${line[0]} in + otp_secret:) secret=${line[1]} ;; + otp_type:) type=${line[1]} ;; + otp_algorithm:) algorithm=${line[1]} ;; + otp_period:) period=${line[1]} ;; + otp_counter:) counter=${line[1]} ;; + otp_digits:) digits=${line[1]} ;; + *) true ;; + esac done <<< "$contents" + + [[ -z $secret ]] && die "Missing otp_secret: line in $passfile" + [[ -z $type ]] && die "Missing otp_type: line in $passfile" + [[ $type = "totp" && -z $algorithm ]] && die "Missing otp_algorithm: line in $passfile" + [[ $type = "hotp" && -z $counter ]] && die "Missing otp_counter: line in $passfile" + + local out + case $type in + totp) out=$($OATH -b --totp="$algorithm" --time-step-size="$period"s --digits="$digits" "$secret") ;; + hotp) otp_increment_counter counter "$counter" "$contents" "$path" "$passfile" > /dev/null \ || die "Failed to increment HOTP counter for $passfile" - out=$($OATH -b --hotp --counter="$counter" --digits="$digits" "$secret") - ;; - *) die "Invalid OTP type '$type'. May be one of 'totp' or 'hotp'" ;; - esac - - if [[ $clip -ne 0 ]]; then - clip "$out" "OTP code for $path" - else - echo "$out" - fi + out=$($OATH -b --hotp --counter="$counter" --digits="$digits" "$secret") + ;; + *) die "Invalid OTP type '$type'. May be one of 'totp' or 'hotp'" ;; + esac + + if [[ $clip -ne 0 ]]; then + clip "$out" "OTP code for $path" + else + echo "$out" + fi } cmd_otp_uri() { - local contents qrcode=0 clip=0 - opts="$($GETOPT -o q -l qrcode -n "$PROGRAM" -- "$@")" - local err=$? - eval set -- "$opts" - while true; do case $1 in - -q|--qrcode) qrcode=1; shift ;; - -c|--clip) clip=1; shift ;; - --) shift; break ;; - esac done - - [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND uri [--clip,-c | --qrcode,-q] pass-name" - - local path="$1" - local passfile="$PREFIX/$path.gpg" - check_sneaky_paths "$path" - [[ ! -f $passfile ]] && die "Passfile not found" - - local secret="" type="" algorithm="" counter="" period=30 digits=6 - - contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile") - while read -r -a line; do case ${line[0]} in - otp_secret:) secret=${line[1]} ;; - otp_type:) type=${line[1]} ;; - otp_algorithm:) algorithm=${line[1]} ;; - otp_period:) period=${line[1]} ;; - otp_counter:) counter=${line[1]} ;; - otp_digits:) digits=${line[1]} ;; - *) true ;; - esac done <<< "$contents" - - local uri - case $type in - totp) uri="otpauth://totp/$path?secret=$secret&algorithm=$algorithm&digits=$digits&period=$period" ;; - hotp) uri="otpauth://hotp/$path?secret=$secret&digits=$digits&counter=$counter" ;; - *) die "Invalid OTP type '$type'. Must be one of 'totp' or 'hotp'" ;; - esac - - if [[ clip -eq 1 ]]; then - clip "$uri" "OTP key URI for $path" - elif [[ qrcode -eq 1 ]]; then - qrcode "$uri" "OTP key URI for $path" - else - echo "$uri" - fi + local contents qrcode=0 clip=0 + opts="$($GETOPT -o q -l qrcode -n "$PROGRAM" -- "$@")" + local err=$? + eval set -- "$opts" + while true; do case $1 in + -q|--qrcode) qrcode=1; shift ;; + -c|--clip) clip=1; shift ;; + --) shift; break ;; + esac done + + [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND uri [--clip,-c | --qrcode,-q] pass-name" + + local path="$1" + local passfile="$PREFIX/$path.gpg" + check_sneaky_paths "$path" + [[ ! -f $passfile ]] && die "Passfile not found" + + local secret="" type="" algorithm="" counter="" period=30 digits=6 + + contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile") + while read -r -a line; do case ${line[0]} in + otp_secret:) secret=${line[1]} ;; + otp_type:) type=${line[1]} ;; + otp_algorithm:) algorithm=${line[1]} ;; + otp_period:) period=${line[1]} ;; + otp_counter:) counter=${line[1]} ;; + otp_digits:) digits=${line[1]} ;; + *) true ;; + esac done <<< "$contents" + + local uri + case $type in + totp) uri="otpauth://totp/$path?secret=$secret&algorithm=$algorithm&digits=$digits&period=$period" ;; + hotp) uri="otpauth://hotp/$path?secret=$secret&digits=$digits&counter=$counter" ;; + *) die "Invalid OTP type '$type'. Must be one of 'totp' or 'hotp'" ;; + esac + + if [[ clip -eq 1 ]]; then + clip "$uri" "OTP key URI for $path" + elif [[ qrcode -eq 1 ]]; then + qrcode "$uri" "OTP key URI for $path" + else + echo "$uri" + fi } cmd_otp_validate() { @@ -329,11 +354,11 @@ cmd_otp_validate() { } case "$1" in - help|--help|-h) shift; cmd_otp_usage "$@" ;; - show) shift; cmd_otp_show "$@" ;; - insert|add) shift; cmd_otp_insert "$@" ;; - uri) shift; cmd_otp_uri "$@" ;; - validate) shift; cmd_otp_validate "$@" ;; - *) cmd_otp_show "$@" ;; + help|--help|-h) shift; cmd_otp_usage "$@" ;; + show) shift; cmd_otp_show "$@" ;; + insert|add) shift; cmd_otp_insert "$@" ;; + uri) shift; cmd_otp_uri "$@" ;; + validate) shift; cmd_otp_validate "$@" ;; + *) cmd_otp_show "$@" ;; esac exit 0 diff --git a/test/insert.t b/test/insert.t index dbd6e2a..eb04898 100755 --- a/test/insert.t +++ b/test/insert.t @@ -33,8 +33,34 @@ test_expect_success 'Force overwrites key URI' ' ' test_expect_success 'Inserts a basic TOTP key' ' + uri="otpauth://totp/passfile?secret=AAAAAAAAAAAAAAAAAAAAA" + + test_pass_init && + "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA passfile && + [[ $("$PASS" show passfile) == "$uri" ]] +' + +test_expect_success 'Inserts a TOTP key with issuer in path' ' + uri="otpauth://totp/example.com:passfile?secret=AAAAAAAAAAAAAAAAAAAAA&issuer=example.com" + + test_pass_init && + "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA example.com/passfile && + [[ $("$PASS" show example.com/passfile) == "$uri" ]] +' + +test_expect_success 'Inserts a TOTP key with issuer in nested path' ' + uri="otpauth://totp/foo:passfile?secret=AAAAAAAAAAAAAAAAAAAAA&issuer=foo" + + test_pass_init && + "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA example.com/foo/passfile && + [[ $("$PASS" show example.com/foo/passfile) == "$uri" ]] +' + +test_expect_success 'Inserts a TOTP key with spaces in path' ' + uri="otpauth://totp/example%20dot%20com:pass%20file?secret=AAAAAAAAAAAAAAAAAAAAA&issuer=example%20dot%20com" test_pass_init && - "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA passfile + "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA "example dot com/pass file" && + [[ $("$PASS" show "example dot com/pass file") == "$uri" ]] ' test_expect_success 'Commits insert to git' ' -- cgit v1.2.3 From d29b61248c87ab29283eb4ccbd037869f0b4df28 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 16:49:01 -0700 Subject: cmd_otp_uri: Show first line starting with otpauth:// in passfile --- otp.bash | 27 ++++++--------------------- test/uri.t | 24 ++++++++++-------------- test/validate.t | 27 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 35 deletions(-) create mode 100755 test/validate.t diff --git a/otp.bash b/otp.bash index dad46b8..539fc7e 100755 --- a/otp.bash +++ b/otp.bash @@ -320,32 +320,17 @@ cmd_otp_uri() { check_sneaky_paths "$path" [[ ! -f $passfile ]] && die "Passfile not found" - local secret="" type="" algorithm="" counter="" period=30 digits=6 - contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile") - while read -r -a line; do case ${line[0]} in - otp_secret:) secret=${line[1]} ;; - otp_type:) type=${line[1]} ;; - otp_algorithm:) algorithm=${line[1]} ;; - otp_period:) period=${line[1]} ;; - otp_counter:) counter=${line[1]} ;; - otp_digits:) digits=${line[1]} ;; - *) true ;; - esac done <<< "$contents" - - local uri - case $type in - totp) uri="otpauth://totp/$path?secret=$secret&algorithm=$algorithm&digits=$digits&period=$period" ;; - hotp) uri="otpauth://hotp/$path?secret=$secret&digits=$digits&counter=$counter" ;; - *) die "Invalid OTP type '$type'. Must be one of 'totp' or 'hotp'" ;; - esac + while read -r -a line; do + [[ "$line" == otpauth://* ]] && otp_parse_uri "$line" + done <<< "$contents" if [[ clip -eq 1 ]]; then - clip "$uri" "OTP key URI for $path" + clip "$otp_uri" "OTP key URI for $path" elif [[ qrcode -eq 1 ]]; then - qrcode "$uri" "OTP key URI for $path" + qrcode "$otp_uri" "OTP key URI for $path" else - echo "$uri" + echo "$otp_uri" fi } diff --git a/test/uri.t b/test/uri.t index 9fbe66c..084b010 100755 --- a/test/uri.t +++ b/test/uri.t @@ -4,24 +4,20 @@ export test_description='Tests pass otp URI parsing' . ./setup.sh -test_expect_success 'Parses a basic TOTP URI' ' - "$PASS" otp validate "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" -' - -test_expect_success 'Parses a complex TOTP URI' ' - "$PASS" otp validate otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 -' +test_expect_success 'Shows key URI in single-line passfile' ' + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" -test_expect_success 'Fails for bogus URL' ' - test_must_fail "$PASS" otp validate https://www.google.com/ + test_pass_init && + "$PASS" otp insert "$uri" passfile && + [[ $("$PASS" otp uri passfile) == "$uri" ]] ' -test_expect_success 'Fails for missing secret' ' - test_must_fail "$PASS" otp validate otpauth://totp/ACME%20Co:john.doe@email.com?issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 -' +test_expect_success 'Shows key URI in multi-line passfile' ' + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" -test_expect_success 'Fails for missing counter' ' - test_must_fail "$PASS" otp validate otpauth://hotp?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ + test_pass_init && + "$PASS" insert -m passfile < <(echo -e "password\nfoo\n$uri\nbar") && + [[ $("$PASS" otp uri passfile) == "$uri" ]] ' test_done diff --git a/test/validate.t b/test/validate.t new file mode 100755 index 0000000..9fbe66c --- /dev/null +++ b/test/validate.t @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +export test_description='Tests pass otp URI parsing' + +. ./setup.sh + +test_expect_success 'Parses a basic TOTP URI' ' + "$PASS" otp validate "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" +' + +test_expect_success 'Parses a complex TOTP URI' ' + "$PASS" otp validate otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 +' + +test_expect_success 'Fails for bogus URL' ' + test_must_fail "$PASS" otp validate https://www.google.com/ +' + +test_expect_success 'Fails for missing secret' ' + test_must_fail "$PASS" otp validate otpauth://totp/ACME%20Co:john.doe@email.com?issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 +' + +test_expect_success 'Fails for missing counter' ' + test_must_fail "$PASS" otp validate otpauth://hotp?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ +' + +test_done -- cgit v1.2.3 From 698ae45a780bcc83d8dadc18aadfb049ec7c127c Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 20:58:45 -0700 Subject: Fix HOTP URI parsing --- otp.bash | 26 +++++++++++++++----------- test/insert.t | 2 +- test/validate.t | 4 ++++ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/otp.bash b/otp.bash index 539fc7e..b95e981 100755 --- a/otp.bash +++ b/otp.bash @@ -38,7 +38,7 @@ otp_parse_uri() { uri="${uri//\`/%60}" uri="${uri//\"/%22}" - local pattern='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?))?(\?([^#&?]+))(&([^#&?]+))*$' + local pattern='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?))?\?(.+)$' [[ "$uri" =~ $pattern ]] || die "Cannot parse OTP key URI: $uri" otp_uri=${BASH_REMATCH[0]} @@ -48,24 +48,28 @@ otp_parse_uri() { otp_accountname=${BASH_REMATCH[6]} [[ -z $otp_accountname ]] && otp_accountname=${BASH_REMATCH[4]} || otp_issuer=${BASH_REMATCH[4]} - local parameters=(${BASH_REMATCH[@]:7}) - pattern='^([^?&=]+)(=(.+))$' - for param in "${parameters[@]}"; do + local p=${BASH_REMATCH[7]} + local IFS=\&; local params=(${p[@]}); unset IFS + + pattern='^(.+)=(.+)$' + for param in "${params[@]}"; do if [[ "$param" =~ $pattern ]]; then case ${BASH_REMATCH[1]} in - secret) otp_secret=${BASH_REMATCH[3]} ;; - digits) otp_digits=${BASH_REMATCH[3]} ;; - algorithm) otp_algorithm=${BASH_REMATCH[3]} ;; - period) otp_period=${BASH_REMATCH[3]} ;; - counter) otp_counter=${BASH_REMATCH[3]} ;; - issuer) otp_issuer=${BASH_REMATCH[3]} ;; + secret) otp_secret=${BASH_REMATCH[2]} ;; + digits) otp_digits=${BASH_REMATCH[2]} ;; + algorithm) otp_algorithm=${BASH_REMATCH[2]} ;; + period) otp_period=${BASH_REMATCH[2]} ;; + counter) otp_counter=${BASH_REMATCH[2]} ;; + issuer) otp_issuer=${BASH_REMATCH[2]} ;; *) ;; esac fi done [[ -z "$otp_secret" ]] && die "Invalid key URI (missing secret): $otp_uri" - [[ "$otp_type" == 'hotp' && -z "$otp_counter" ]] && die "Invalid key URI (missing counter): $otp_uri" + + pattern='^[0-9]+$' + [[ "$otp_type" == 'hotp' ]] && [[ ! "$otp_counter" =~ $pattern ]] && die "Invalid key URI (missing counter): $otp_uri" } otp_build_uri() { diff --git a/test/insert.t b/test/insert.t index eb04898..228a8ec 100755 --- a/test/insert.t +++ b/test/insert.t @@ -67,7 +67,7 @@ test_expect_success 'Commits insert to git' ' test_pass_init && pass git init && "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA passfile && - git log --no-decorate -1 | grep "Add given OTP secret for passfile to store." + git log --no-decorate -1 | grep "Add OTP secret for passfile to store." ' test_done diff --git a/test/validate.t b/test/validate.t index 9fbe66c..6d05fdf 100755 --- a/test/validate.t +++ b/test/validate.t @@ -12,6 +12,10 @@ test_expect_success 'Parses a complex TOTP URI' ' "$PASS" otp validate otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 ' +test_expect_success 'Parses a basic HOTP URI' ' + "$PASS" otp validate "otpauth://hotp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&counter=10&issuer=Example" +' + test_expect_success 'Fails for bogus URL' ' test_must_fail "$PASS" otp validate https://www.google.com/ ' -- cgit v1.2.3 From eecfd18bbe4987e1f49f060d0329b4668bda747f Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 21:00:12 -0700 Subject: Parse key URIs to generate OTP codes --- otp.bash | 91 ++++++++++++++++++++++++++++++------------------------------- test/code.t | 27 ++++++++++++++++++ 2 files changed, 71 insertions(+), 47 deletions(-) create mode 100755 test/code.t diff --git a/otp.bash b/otp.bash index b95e981..a2007c9 100755 --- a/otp.bash +++ b/otp.bash @@ -95,10 +95,10 @@ otp_build_uri() { fi [[ -z "$secret" ]] && die "Missing secret"; uri+="?secret=$secret" - [[ -n "$algorithm" ]] && uri+="&algorithm=$algorithm" case "$1" in totp) + [[ -n "$algorithm" ]] && uri+="&algorithm=$algorithm" [[ -n "$digits" ]] && uri+="&digits=$digits" [[ -n "$period" ]] && uri+="&period=$period" ;; @@ -115,28 +115,12 @@ otp_build_uri() { echo "$uri" } -otp_increment_counter() { - local ret=$1 - local counter=$2 contents="$3" path="$4" passfile="$5" - - local inc=$((counter+1)) - - contents=${contents//otp_counter: $counter/otp_counter: $inc} - - set_gpg_recipients "$(dirname "$path")" - - $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted." - - git_add_file "$passfile" "Update HOTP counter value for $path." - - eval "$ret='$inc'" -} - otp_insert() { local path="${1%/}" local passfile="$PREFIX/$path.gpg" local force=$2 local contents="$3" + local message="$4" check_sneaky_paths "$path" set_git "$passfile" @@ -148,7 +132,9 @@ otp_insert() { $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted." - git_add_file "$passfile" "Add given OTP secret for $path to store." + [[ -z "$message" ]] && message="Add OTP secret for $path to store." + + git_add_file "$passfile" "$message" } otp_insert_uri() { @@ -256,8 +242,8 @@ cmd_otp_insert() { esac } -cmd_otp_show() { - local opts contents clip=0 secret="" type="" algorithm="" counter="" period=30 digits=6 +cmd_otp_code() { + local opts clip=0 opts="$($GETOPT -o c -l clip -n "$PROGRAM" -- "$@")" local err=$? eval set -- "$opts" @@ -266,7 +252,7 @@ cmd_otp_show() { --) shift; break ;; esac done - [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND show [--clip,-c] pass-name" + [[ $err -ne 0 || $# -ne 1 ]] && die "Usage: $PROGRAM $COMMAND [--clip,-c] pass-name" local path="$1" local passfile="$PREFIX/$path.gpg" @@ -274,31 +260,39 @@ cmd_otp_show() { [[ ! -f $passfile ]] && die "Passfile not found" contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile") - while read -r -a line; do case ${line[0]} in - otp_secret:) secret=${line[1]} ;; - otp_type:) type=${line[1]} ;; - otp_algorithm:) algorithm=${line[1]} ;; - otp_period:) period=${line[1]} ;; - otp_counter:) counter=${line[1]} ;; - otp_digits:) digits=${line[1]} ;; - *) true ;; - esac done <<< "$contents" - - [[ -z $secret ]] && die "Missing otp_secret: line in $passfile" - [[ -z $type ]] && die "Missing otp_type: line in $passfile" - [[ $type = "totp" && -z $algorithm ]] && die "Missing otp_algorithm: line in $passfile" - [[ $type = "hotp" && -z $counter ]] && die "Missing otp_counter: line in $passfile" - - local out - case $type in - totp) out=$($OATH -b --totp="$algorithm" --time-step-size="$period"s --digits="$digits" "$secret") ;; - hotp) otp_increment_counter counter "$counter" "$contents" "$path" "$passfile" > /dev/null \ - || die "Failed to increment HOTP counter for $passfile" - out=$($OATH -b --hotp --counter="$counter" --digits="$digits" "$secret") + while read -r -a line; do + if [[ "$line" == otpauth://* ]]; then + otp_parse_uri "$line" + break + fi + done <<< "$contents" + + local cmd + case "$otp_type" in + totp) + cmd="$OATH -b --totp" + [[ -n "$otp_algorithm" ]] && cmd+="=$otp_algorithm" + [[ -n "$otp_period" ]] && cmd+=" --time-step-size=$period"s + [[ -n "$otp_digits" ]] && cmd+=" --digits=$digits" + cmd+=" $otp_secret" + ;; + + hotp) + local counter=$((otp_counter+1)) + cmd="$OATH -b --hotp --counter=$counter" + [[ -n "$otp_digits" ]] && cmd+=" --digits=$digits" + cmd+=" $otp_secret" ;; - *) die "Invalid OTP type '$type'. May be one of 'totp' or 'hotp'" ;; esac + local out; out=$($cmd) || die "Failed to generate OTP code for $path" + + 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." + fi + if [[ $clip -ne 0 ]]; then clip "$out" "OTP code for $path" else @@ -326,7 +320,10 @@ cmd_otp_uri() { contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile") while read -r -a line; do - [[ "$line" == otpauth://* ]] && otp_parse_uri "$line" + if [[ "$line" == otpauth://* ]]; then + otp_parse_uri "$line" + break + fi done <<< "$contents" if [[ clip -eq 1 ]]; then @@ -344,10 +341,10 @@ cmd_otp_validate() { case "$1" in help|--help|-h) shift; cmd_otp_usage "$@" ;; - show) shift; cmd_otp_show "$@" ;; insert|add) shift; cmd_otp_insert "$@" ;; uri) shift; cmd_otp_uri "$@" ;; validate) shift; cmd_otp_validate "$@" ;; - *) cmd_otp_show "$@" ;; + code|show) shift; cmd_otp_code "$@" ;; + *) cmd_otp_code "$@" ;; esac exit 0 diff --git a/test/code.t b/test/code.t new file mode 100755 index 0000000..095cdd5 --- /dev/null +++ b/test/code.t @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +export test_description='Tests pass otp code generation' + +. ./setup.sh + +test_expect_success 'Generates TOTP code' ' + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + "$PASS" otp insert "$uri" passfile && + code=$("$PASS" otp passfile) && + [[ ${#code} -eq 6 ]] +' + +test_expect_success 'Generates HOTP code and increments counter' ' + 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" + + test_pass_init && + "$PASS" otp insert "$uri" passfile && + code=$("$PASS" otp passfile) && + [[ ${#code} -eq 6 ]] && + [[ $("$PASS" otp uri passfile) == "$inc" ]] +' + +test_done -- cgit v1.2.3 From ec03527ed099b5e87e0fd8f5d0da33db8407bc04 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 21:03:35 -0700 Subject: Add oathtool to Travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ed6b706..cd537e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ addons: sources: - debian-sid packages: + - oathtool - shellcheck before_script: -- cgit v1.2.3 From 045214ea388939021967a0524b49b6e1539f3d19 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 21:42:29 -0700 Subject: Update documentation. --- otp.bash | 4 ++-- pass-otp.1 | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/otp.bash b/otp.bash index a2007c9..301ad99 100755 --- a/otp.bash +++ b/otp.bash @@ -175,10 +175,10 @@ otp_insert_spec() { esac done [[ $type == "totp" && ($err -ne 0 || $# -ne 1) ]] && - die "Usage: $PROGRAM $COMMAND insert totp [--secret=key,s key] [--algorithm=algorithm,-a algorithm] [--period=seconds,-p seconds] [--digits=digits,-d digits] [--force,-f] pass-name" + die "Usage: $PROGRAM $COMMAND insert totp [--secret=key,s key] [--issuer=issuer,-i issuer] [--accountname=name,-n name] [--algorithm=algorithm,-a algorithm] [--period=seconds,-p seconds] [--digits=digits,-d digits] [--force,-f] pass-name" [[ $type == "hotp" && ($err -ne 0 || $# -ne 2) ]] && - die "Usage: $PROGRAM $COMMAND insert hotp [--secret=key,s key] [--digits=digits,-d digits] [--force,-f] pass-name counter" + die "Usage: $PROGRAM $COMMAND insert hotp [--secret=key,s key] [--issuer=issuer,-i issuer] [--accountname=accountname,-n accountname] [--digits=digits,-d digits] [--force,-f] pass-name counter" local path="$1" counter="$2" diff --git a/pass-otp.1 b/pass-otp.1 index 3b69fd6..8e67621 100644 --- a/pass-otp.1 +++ b/pass-otp.1 @@ -37,7 +37,19 @@ and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds. .TP -\fBotp insert totp\fP [ \fI--secret\fP=\fIkey\fP, \fI-s\fP \fIkey\fP ] [ \fI--algorithm\fP=\fIalgorithm\fP, \fI-a\fP \fIalgorithm\fP ] [ \fI--period\fP=\fIperiod\fP, \fI-p\fP \fIperiod\fP ] [ \fI--digits\fP=\fIdigits\fP, \fI-d\fP \fIdigits\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP +\fBotp insert\fP [ \fI--force\fP, \fI-f\fP ] \fIuri\fP \fIpass-name\fP + +Insert a new OTP secret specified by \fIuri\fP into the password store at +\fIpass-name\fP. \fIuri\fP 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 . +Prompt before overwriting an existing password, unless +\fI--force\fP or \fI-f\fP is specified. This command is alternatively named +\fBadd\fP. + +.TP +\fBotp insert totp\fP [ \fI--secret\fP=\fIkey\fP, \fI-s\fP \fIkey\fP ] [ \fI--issuer\fP=\fIissuer\fP, \fI-i\fP \fIissuer\fP ] [ \fI--accountname\fP=\fIname\fP, \fI-n\fP \fIname\fP ] [ \fI--algorithm\fP=\fIalgorithm\fP, \fI-a\fP \fIalgorithm\fP ] [ \fI--period\fP=\fIperiod\fP, \fI-p\fP \fIperiod\fP ] [ \fI--digits\fP=\fIdigits\fP, \fI-d\fP \fIdigits\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP Insert a new TOTP secret into the password store called \fIpass-name\fP. If \fI--secret\fP or \fI-s\fP are not specified, this will read \fIKEY\fP from @@ -46,7 +58,7 @@ standard in. Prompt before overwriting an existing password, unless \fBadd totp\fP. .TP -\fBotp insert hotp\fP [ \fI--secret\fP=\fIkey\fP, \fI-s\fP \fIkey\fP ] [ \fI--digits\fP=\fIdigits\fP, \fI-d\fP \fIdigits\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP \fIcounter\fP +\fBotp insert hotp\fP [ \fI--secret\fP=\fIkey\fP, \fI-s\fP \fIkey\fP ] [ \fI--issuer\fP=\fIissuer\fP, \fI-i\fP \fIissuer\fP ] [ \fI--accountname\fP=\fIname\fP, \fI-n\fP \fIname\fP ] [ \fI--digits\fP=\fIdigits\fP, \fI-d\fP \fIdigits\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP \fIcounter\fP Insert a new HOTP secret into the password store called \fIpass-name\fP. A \fIcounter\fP argument is required, which is an integer specifying the initial -- cgit v1.2.3 From 318febc9ec78ab35ae4021472bd8ce29a5389c4b Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 22:01:42 -0700 Subject: Remove otp_insert_spec and related insert commands --- otp.bash | 175 ++++++++++------------------------------------------------ pass-otp.1 | 66 ++++++---------------- test/insert.t | 38 ------------- 3 files changed, 45 insertions(+), 234 deletions(-) diff --git a/otp.bash b/otp.bash index 301ad99..664a37b 100755 --- a/otp.bash +++ b/otp.bash @@ -18,17 +18,6 @@ OATH=$(which oathtool) -otp_urlencode() { - local LANG=C - for ((i=0; i<${#1}; i++)); do - if [[ ${1:$i:1} =~ ^[a-zA-Z0-9\.\~_-]$ ]]; then - printf "%s" "${1:$i:1}" - else - printf '%%%02X' "'${1:$i:1}" - fi - done -} - # Parse a Key URI per: https://github.com/google/google-authenticator/wiki/Key-Uri-Format # Vars are consumed by caller # shellcheck disable=SC2034 @@ -47,6 +36,7 @@ otp_parse_uri() { otp_accountname=${BASH_REMATCH[6]} [[ -z $otp_accountname ]] && otp_accountname=${BASH_REMATCH[4]} || otp_issuer=${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 @@ -72,49 +62,6 @@ otp_parse_uri() { [[ "$otp_type" == 'hotp' ]] && [[ ! "$otp_counter" =~ $pattern ]] && die "Invalid key URI (missing counter): $otp_uri" } -otp_build_uri() { - local type="$1" issuer="$2" accountname="$3" secret="$4" algorithm="$5" \ - digits="$6" period="$7" counter="$8" - - local uri="otpauth://$type/" - - local pattern='^[^:]+$' - if [[ -n "$issuer" ]]; then - [[ "$issuer" =~ $pattern ]] || die "Invalid character in issuer: ':'" - issuer=$(otp_urlencode "$issuer") - fi - - [[ -z "$accountname" ]] && die "Missing accountname" - [[ "$accountname" =~ $pattern ]] || die "Invalid character in accountname: ':'" - accountname=$(otp_urlencode "$accountname") - - if [[ -n "$issuer" ]]; then - uri+="$issuer:$accountname" - else - uri+="$accountname" - fi - - [[ -z "$secret" ]] && die "Missing secret"; uri+="?secret=$secret" - - case "$1" in - totp) - [[ -n "$algorithm" ]] && uri+="&algorithm=$algorithm" - [[ -n "$digits" ]] && uri+="&digits=$digits" - [[ -n "$period" ]] && uri+="&period=$period" - ;; - - hotp) - [[ -z "$counter" ]] && die "Missing counter"; uri+="&counter=$counter" - ;; - - *) die "Invalid OTP type '$1'" ;; - esac - - [[ -n "$issuer" ]] && uri+="&issuer=$issuer" - - echo "$uri" -} - otp_insert() { local path="${1%/}" local passfile="$PREFIX/$path.gpg" @@ -132,103 +79,23 @@ otp_insert() { $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted." - [[ -z "$message" ]] && message="Add OTP secret for $path to store." - git_add_file "$passfile" "$message" } -otp_insert_uri() { - local opts force=0 - opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")" - local err=$? - eval set -- "$opts" - while true; do case $1 in - -f|--force) force=1; shift ;; - --) shift; break ;; - esac done - - [[ $err -ne 0 || $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] uri pass-name" - - local uri="$1" - - otp_parse_uri "$uri" - - otp_insert "$2" $force "$otp_uri" -} - -otp_insert_spec() { - local opts contents secret issuer accountname algorithm period digits counter force=0 - local type="$1"; shift - - opts="$($GETOPT -o s:i:n:a:p:d:f -l secret:,issuer:,accountname:,algorithm:,period:,digits:,force -n "$PROGRAM" -- "$@")" - local err=$? - eval set -- "$opts" - while true; do case "$1" in - -s|--secret) secret="$2"; shift 2 ;; - -i|--issuer) issuer="$2"; shift 2 ;; - -n|--accountname) accountname="$2"; shift 2 ;; - -a|--algorithm) algorithm="$2"; shift 2 ;; - -p|--period) period="$2"; shift 2 ;; - -d|--digits) digits="$2"; shift 2 ;; - -f|--force) force=1; shift ;; - --) shift; break ;; - esac done - - [[ $type == "totp" && ($err -ne 0 || $# -ne 1) ]] && - die "Usage: $PROGRAM $COMMAND insert totp [--secret=key,s key] [--issuer=issuer,-i issuer] [--accountname=name,-n name] [--algorithm=algorithm,-a algorithm] [--period=seconds,-p seconds] [--digits=digits,-d digits] [--force,-f] pass-name" - - [[ $type == "hotp" && ($err -ne 0 || $# -ne 2) ]] && - die "Usage: $PROGRAM $COMMAND insert hotp [--secret=key,s key] [--issuer=issuer,-i issuer] [--accountname=accountname,-n accountname] [--digits=digits,-d digits] [--force,-f] pass-name counter" - - local path="$1" counter="$2" - - [[ -n "$algorithm" ]] && case $algorithm in - sha1|sha256|sha512) ;; - *) die "Invalid algorithm '$algorithm'. May be one of 'sha1', 'sha256', or 'sha512'" ;; - esac - - [[ -n "$digits" ]] && case $digits in - 6|8) ;; - *) die "Invalid digits '$digits'. May be one of '6' or '8'" ;; - esac - - if [[ -z $secret ]]; then - read -r -p "Enter secret (base32-encoded): " -s secret || die "Missing secret" - fi - - # Populate issuer and accountname from either options or path - if [[ -z $accountname ]]; then - accountname="$(basename "$path")" - if [[ -z "$issuer" ]]; then - issuer="$(basename "$(dirname "$path")")" - [[ "$issuer" == "." ]] && unset issuer - fi - fi - - local uri; uri=$(otp_build_uri "$type" "$issuer" "$accountname" "$secret" "$algorithm" "$period" "$digits" "$counter") - - otp_insert "$1" $force "$uri" -} - cmd_otp_usage() { cat <<-_EOF Usage: $PROGRAM otp [show] [--clip,-c] pass-name 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 totp [--secret=key,-s key] [--algorithm alg,-a alg] - [--period=seconds,-p seconds] - [--digits=digits,-d digits] [--force,-f] pass-name - Insert new TOTP secret. Prompt before overwriting existing password + $PROGRAM otp insert [--force,-f] uri pass-name + Insert new OTP key URI. Prompt before overwriting existing password unless forced. - $PROGRAM otp insert hotp [--secret=secret,-s secret] - [--digits=digits,-d digits] [--force,-f] - pass-name counter - Insert new HOTP secret with initial counter. Prompt before overwriting - existing password unless forced. $PROGRAM otp uri [--clip,-c] [--qrcode,-q] pass-name - Create a secret key URI suitable for importing into other TOTP clients. - Optionally, put it on the clipboard, or display a QR code. + Display the key URI stored in pass-name. Optionally, put it on the + clipboard, or display a QR code. + $PROGRAM otp validate uri + Test if the given URI is a valid OTP key URI. More information may be found in the pass-otp(1) man page. _EOF @@ -236,13 +103,27 @@ _EOF } cmd_otp_insert() { - case "$1" in - totp|hotp) otp_insert_spec "$@" ;; - *) otp_insert_uri "$@" ;; - esac + local opts force=0 + opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")" + local err=$? + eval set -- "$opts" + while true; do case $1 in + -f|--force) force=1; shift ;; + --) shift; break ;; + esac done + + [[ $err -ne 0 || $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] uri pass-name" + + local uri="$1" + + otp_parse_uri "$uri" + + otp_insert "$2" $force "$otp_uri" "Add OTP secret for $2 to store." } cmd_otp_code() { + [[ -z "$OATH" ]] && die "Failed to generate OTP code: oathtool is not installed." + local opts clip=0 opts="$($GETOPT -o c -l clip -n "$PROGRAM" -- "$@")" local err=$? @@ -272,15 +153,15 @@ cmd_otp_code() { totp) cmd="$OATH -b --totp" [[ -n "$otp_algorithm" ]] && cmd+="=$otp_algorithm" - [[ -n "$otp_period" ]] && cmd+=" --time-step-size=$period"s - [[ -n "$otp_digits" ]] && cmd+=" --digits=$digits" + [[ -n "$otp_period" ]] && cmd+=" --time-step-size=$otp_period"s + [[ -n "$otp_digits" ]] && cmd+=" --digits=$otp_digits" cmd+=" $otp_secret" ;; hotp) local counter=$((otp_counter+1)) cmd="$OATH -b --hotp --counter=$counter" - [[ -n "$otp_digits" ]] && cmd+=" --digits=$digits" + [[ -n "$otp_digits" ]] && cmd+=" --digits=$otp_digits" cmd+=" $otp_secret" ;; esac diff --git a/pass-otp.1 b/pass-otp.1 index 8e67621..ca298dc 100644 --- a/pass-otp.1 +++ b/pass-otp.1 @@ -23,18 +23,18 @@ utility with the command for adding OTP secrets, generating OTP codes, and displaying secret key URIs using the standard \fIotpauth://\fP scheme. -If no COMMAND is specified, COMMAND defaults to \fBshow\fP. +If no COMMAND is specified, COMMAND defaults to \fBcode\fP. .SH COMMANDS .TP -\fBotp show\fP [ \fI--clip\fP, \fI-c\fP ] \fIpass-name\fP +\fBotp code\fP [ \fI--clip\fP, \fI-c\fP ] \fIpass-name\fP Generate and print an OTP code from the secret key stored in \fIpass-name\fP. If \fI--clip\fP or \fI-c\fP is specified, do not print the code but instead copy it to the clipboard using .BR xclip (1) and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) -seconds. +seconds. This command is alternatively named \fBshow\fP. .TP \fBotp insert\fP [ \fI--force\fP, \fI-f\fP ] \fIuri\fP \fIpass-name\fP @@ -49,31 +49,11 @@ Prompt before overwriting an existing password, unless \fBadd\fP. .TP -\fBotp insert totp\fP [ \fI--secret\fP=\fIkey\fP, \fI-s\fP \fIkey\fP ] [ \fI--issuer\fP=\fIissuer\fP, \fI-i\fP \fIissuer\fP ] [ \fI--accountname\fP=\fIname\fP, \fI-n\fP \fIname\fP ] [ \fI--algorithm\fP=\fIalgorithm\fP, \fI-a\fP \fIalgorithm\fP ] [ \fI--period\fP=\fIperiod\fP, \fI-p\fP \fIperiod\fP ] [ \fI--digits\fP=\fIdigits\fP, \fI-d\fP \fIdigits\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP +\fBotp uri\fP [ \fI--clip\fP, \fI-c\fP | \fI--qrcode\fP, \fI-q\fP ] \fIpass-name\fP -Insert a new TOTP secret into the password store called \fIpass-name\fP. If -\fI--secret\fP or \fI-s\fP are not specified, this will read \fIKEY\fP from -standard in. Prompt before overwriting an existing password, unless -\fI--force\fP or \fI-f\fP is specified. This command is alternatively named -\fBadd totp\fP. - -.TP -\fBotp insert hotp\fP [ \fI--secret\fP=\fIkey\fP, \fI-s\fP \fIkey\fP ] [ \fI--issuer\fP=\fIissuer\fP, \fI-i\fP \fIissuer\fP ] [ \fI--accountname\fP=\fIname\fP, \fI-n\fP \fIname\fP ] [ \fI--digits\fP=\fIdigits\fP, \fI-d\fP \fIdigits\fP ] [ \fI--force\fP, \fI-f\fP ] \fIpass-name\fP \fIcounter\fP - -Insert a new HOTP secret into the password store called \fIpass-name\fP. A -\fIcounter\fP argument is required, which is an integer specifying the initial -HOTP counter stored alongside the secret. If -\fI--secret\fP or \fI-s\fP are not specified, this will read \fIKEY\fP from -standard in. Prompt before overwriting an existing password, unless -\fI--force\fP or \fI-f\fP is specified. This command is alternatively named -\fBadd hotp\fP. - -.TP -\fBotp uri\fP [ \fI--clip\fP, \fI-c\fP | \fI--qrcode\fP, \fI-q\fP ] pass-name - -Create and print a URI encoding the secret key and OTP parameters using the -standard \fIotpauth://\fP scheme. If \fI--clip\fP or \fI-c\fP is specified, do -not print the URI but instead copy it to the clipboard using +Print the key URI stored in \fIpass-name\fP to stdout. If \fI--clip\fP or +\fI-c\fP is specified, do not print the URI but instead copy it to the clipboard +using .BR xclip (1) and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) seconds. If \fI--qrcode\fP or \fI-q\fP is specified, do not print the URI but @@ -81,6 +61,14 @@ instead display a QR code using .BR qrencode (1) either to the terminal or graphically if supported. +.TP +\fBotp validate\fP \fIuri\fP + +Test a URI string for validity according to the Key Uri Format. For more +information about this format, see the documentation at +.UR https://\:github.\:com/\:google/\:google-authenticator/\:wiki/\:Key-Uri-Format +.UE . + .SH OPTIONS .TP @@ -89,30 +77,10 @@ Put the OTP code in the clipboard. .TP \fB\-f\fP, \fB--force\fP -Force to update and do not wait for user instruction. - -.TP -\fB-s\fP \fIkey\fP, \fB--secret\fR=\fIkey\fP -Provide a secret \fIkey\fP. This key must be base32-encoded. - -.TP -\fB-a\fP \fIalgorithm\fP, \fB--algorithm\fP=\fIalgorithm\fP -Specify the \fIalgorithm\fP for a TOTP secret. Accepted values are \fIsha1\fP, -\fIsha256\fP, and \fIsha512\fP. This option defaults to \fIsha1\fP. - -.TP -\fB-p\fP \fIperiod\fP, \fB--period\fP=\fIperiod\fP -Specify the \fIperiod\fP for a TOTP secret, in seconds. This option defaults to -\fI30\fP. - -.TP -\fB-d\fP \fIdigits\fP, \fB--digits\fP=\fIdigits\fP -Specify the number of \fIdigits\fP this secret should generate when used with -\fBshow\fP. Accepted values are \fI6\fP and \fI8\fP. This option defaults to -\fI6\fP. +Force update and do not wait for user instruction. .TP -\fB\-h\fB, \-\-help\fR +\fBhelp\fP, \fB\-h\fB, \-\-help\fR Show usage message. .SH SEE ALSO diff --git a/test/insert.t b/test/insert.t index 228a8ec..da6477c 100755 --- a/test/insert.t +++ b/test/insert.t @@ -32,42 +32,4 @@ test_expect_success 'Force overwrites key URI' ' [[ $("$PASS" show passfile) == "$uri2" ]] ' -test_expect_success 'Inserts a basic TOTP key' ' - uri="otpauth://totp/passfile?secret=AAAAAAAAAAAAAAAAAAAAA" - - test_pass_init && - "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA passfile && - [[ $("$PASS" show passfile) == "$uri" ]] -' - -test_expect_success 'Inserts a TOTP key with issuer in path' ' - uri="otpauth://totp/example.com:passfile?secret=AAAAAAAAAAAAAAAAAAAAA&issuer=example.com" - - test_pass_init && - "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA example.com/passfile && - [[ $("$PASS" show example.com/passfile) == "$uri" ]] -' - -test_expect_success 'Inserts a TOTP key with issuer in nested path' ' - uri="otpauth://totp/foo:passfile?secret=AAAAAAAAAAAAAAAAAAAAA&issuer=foo" - - test_pass_init && - "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA example.com/foo/passfile && - [[ $("$PASS" show example.com/foo/passfile) == "$uri" ]] -' - -test_expect_success 'Inserts a TOTP key with spaces in path' ' - uri="otpauth://totp/example%20dot%20com:pass%20file?secret=AAAAAAAAAAAAAAAAAAAAA&issuer=example%20dot%20com" - test_pass_init && - "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA "example dot com/pass file" && - [[ $("$PASS" show "example dot com/pass file") == "$uri" ]] -' - -test_expect_success 'Commits insert to git' ' - test_pass_init && - pass git init && - "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA passfile && - git log --no-decorate -1 | grep "Add OTP secret for passfile to store." -' - test_done -- cgit v1.2.3 From acfab191e6bb33f840f0a7106c3eb4cd1ab76267 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 23:09:24 -0700 Subject: Support --echo and reading key from stdin --- otp.bash | 50 +++++++++++++++++++++++++++++++++++++------------- pass-otp.1 | 14 ++++++++------ test/insert.t | 24 ++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 19 deletions(-) diff --git a/otp.bash b/otp.bash index 664a37b..8b80554 100755 --- a/otp.bash +++ b/otp.bash @@ -88,9 +88,10 @@ Usage: $PROGRAM otp [show] [--clip,-c] pass-name 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] uri pass-name - Insert new OTP key URI. Prompt before overwriting existing password - unless forced. + $PROGRAM otp insert [--force,-f] [--echo,-e] [uri] pass-name + Insert a new OTP key URI. If one is not supplied, it will be read from + stdin. Optionally, echo the input. Prompt before overwriting existing + password unless forced. $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. @@ -103,22 +104,45 @@ _EOF } cmd_otp_insert() { - local opts force=0 - opts="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@")" + 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 ;; - --) shift; break ;; - esac done - - [[ $err -ne 0 || $# -ne 2 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] uri pass-name" + -f|--force) force=1; shift ;; + -e|--echo) echo=1; shift ;; + --) shift; break ;; + esac done - local uri="$1" + [[ $err -ne 0 || ($# -ne 1 && $# -ne 2) ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] [uri] pass-name" + + local path uri + if [[ $# -eq 1 ]]; then + path="$1" + if [[ -t 0 ]]; then + if [[ $echo -eq 0 ]]; then + while true; do + read -r -p "Enter otpauth:// URI for $path: " -s uri || exit 1 + echo + read -r -p "Retype otpauth:// URI for $path: " -s uri_again || exit 1 + echo + [[ "$uri" == "$uri_again" ]] && break + die "Error: the entered URIs do not match." + done + else + read -r -p "Enter otpauth:// URI for $path: " -e uri + fi + else + read -r uri + fi + else + uri="$1" + path="$2" + fi otp_parse_uri "$uri" - otp_insert "$2" $force "$otp_uri" "Add OTP secret for $2 to store." + otp_insert "$path" $force "$otp_uri" "Add OTP secret for $2 to store." } cmd_otp_code() { @@ -217,7 +241,7 @@ cmd_otp_uri() { } cmd_otp_validate() { - otp_parse_uri "$1" + otp_parse_uri "$1" } case "$1" in diff --git a/pass-otp.1 b/pass-otp.1 index ca298dc..69ff10d 100644 --- a/pass-otp.1 +++ b/pass-otp.1 @@ -1,4 +1,4 @@ -.TH PASS-OTP 1 "2017 February 14" "Password store OTP extension" +.TH PASS-OTP 1 "2017 March 19" "Password store OTP extension" .SH NAME pass-otp - A \fBpass\fP(1) extension for managing one-time-password (OTP) tokens. @@ -30,21 +30,23 @@ If no COMMAND is specified, COMMAND defaults to \fBcode\fP. .TP \fBotp code\fP [ \fI--clip\fP, \fI-c\fP ] \fIpass-name\fP -Generate and print an OTP code from the secret key stored in \fIpass-name\fP. If -\fI--clip\fP or \fI-c\fP is specified, do not print the code but instead copy it to the clipboard using -.BR xclip (1) +Generate and print an OTP code from the secret key stored in \fIpass-name\fP +using \fBoathtool\fP(1). If \fI--clip\fP or \fI-c\fP is specified, do not print +the code but instead copy it to the clipboard using \fBxclip\fP(1) 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 ] \fIuri\fP \fIpass-name\fP +\fBotp insert\fP [ \fI--force\fP, \fI-f\fP ] [ \fI--echo\fP, \fI-e\fP ] [ \fIuri\fP ] \fIpass-name\fP Insert a new OTP secret specified by \fIuri\fP into the password store at \fIpass-name\fP. \fIuri\fP 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 . -Prompt before overwriting an existing password, unless +If \fIuri\fP is not specified, it will be consumed from stdin; specify +\fI--echo\fP or \fI-e\fP to show a visible prompt when running this command +interactively. Prompt before overwriting an existing password, unless \fI--force\fP or \fI-f\fP is specified. This command is alternatively named \fBadd\fP. diff --git a/test/insert.t b/test/insert.t index da6477c..0c19d11 100755 --- a/test/insert.t +++ b/test/insert.t @@ -32,4 +32,28 @@ test_expect_success 'Force overwrites key URI' ' [[ $("$PASS" show passfile) == "$uri2" ]] ' +test_expect_success 'Reads non-terminal input' ' + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + "$PASS" otp insert passfile <<< "$uri" && + [[ $("$PASS" show passfile) == "$uri" ]] +' + +test_expect_success 'Reads terminal input in noecho mode' ' + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + test_faketty "$PASS" otp insert passfile < <(echo -ne "$uri\n$uri\n") && + [[ $("$PASS" show passfile) == "$uri" ]] +' + +test_expect_success 'Reads terminal input in echo mode' ' + uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example" + + test_pass_init && + test_faketty "$PASS" otp insert -e passfile <<< "$uri" && + [[ $("$PASS" show passfile) == "$uri" ]] +' + test_done -- cgit v1.2.3 From 224c4dc57ee42215806c6fb8b92e64ddebaa7fd7 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 19 Mar 2017 23:48:03 -0700 Subject: Use expect for interactive tests --- .travis.yml | 1 + otp.bash | 14 ++++++-------- test/insert.t | 45 ++++++++++++++++++++++++++++++++++++++------- test/setup.sh | 6 ++---- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index cd537e0..4122f71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ addons: sources: - debian-sid packages: + - expect - oathtool - shellcheck diff --git a/otp.bash b/otp.bash index 8b80554..91661d8 100755 --- a/otp.bash +++ b/otp.bash @@ -121,16 +121,14 @@ cmd_otp_insert() { path="$1" if [[ -t 0 ]]; then if [[ $echo -eq 0 ]]; then - while true; do - read -r -p "Enter otpauth:// URI for $path: " -s uri || exit 1 - echo - read -r -p "Retype otpauth:// URI for $path: " -s uri_again || exit 1 - echo - [[ "$uri" == "$uri_again" ]] && break - die "Error: the entered URIs do not match." - done + read -r -p "Enter otpauth:// URI for $path: " -s uri || exit 1 + echo + read -r -p "Retype otpauth:// URI for $path: " -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 $path: " -e uri + echo fi else read -r uri diff --git a/test/insert.t b/test/insert.t index 0c19d11..b4af4f5 100755 --- a/test/insert.t +++ b/test/insert.t @@ -16,9 +16,18 @@ test_expect_success 'Prompts before overwriting key URI' ' 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" otp insert "$uri1" passfile && - test_faketty "$PASS" otp insert "$uri2" passfile < <(echo n) && + test_pass_init + "$PASS" otp insert "$uri1" passfile + expect < Date: Mon, 20 Mar 2017 09:37:35 -0700 Subject: Update README --- README.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++++-------------- otp.bash | 7 ++-- 2 files changed, 90 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index ad3a121..6c0ad4d 100644 --- a/README.md +++ b/README.md @@ -9,52 +9,84 @@ one-time-password (OTP) tokens. ``` Usage: - pass otp [show] [--clip,-c] pass-name + + pass otp [code] [--clip,-c] pass-name Generate an OTP code and optionally put it on the clipboard. If put on the clipboard, it will be cleared in 45 seconds. - pass otp insert totp [--secret=key,-s key] [--algorithm alg,-a alg] - [--period=seconds,-p seconds] - [--digits=digits,-d digits] [--force,-f] pass-name - Insert new TOTP secret. Prompt before overwriting existing password - unless forced. - pass otp insert hotp [--secret=secret,-s secret] - [--digits=digits,-d digits] [--force,-f] - pass-name counter - Insert new HOTP secret with initial counter. Prompt before overwriting - existing password unless forced. + + pass otp insert [--force,-f] [--echo,-e] [uri] pass-name + Insert a new OTP key URI. If one is not supplied, it will be read from + stdin. Optionally, echo the input. Prompt before overwriting existing + password unless forced. + pass otp uri [--clip,-c] [--qrcode,-q] pass-name - Create a secret key URI suitable for importing into other TOTP clients. - Optionally, put it on the clipboard, or display a QR code. + Display the key URI stored in pass-name. Optionally, put it on the + clipboard, or display a QR code. + + pass otp validate uri + Test if the given URI is a valid OTP key URI. More information may be found in the pass-otp(1) man page. ``` -## Example +## Examples Insert a TOTP token: ``` -$ pass otp insert totp -s AAAAAAAAAAAAAAAAAAAAA totp-secret -[master 4f9b989] Add given OTP secret for totp-secret to store. - 1 file changed, 0 insertions(+), 0 deletions(-) - create mode 100644 totp-secret.gpg +$ pass otp insert otpauth://totp/totp-secret?secret=AAAAAAAAAAAAAAAA totp-secret +``` +Have `pass-otp` prompt you for a token (avoids potential shell history leakage): -$ pass show totp-secret -otp_secret: AAAAAAAAAAAAAAAAAAAAA -otp_type: totp -otp_algorithm: sha1 -otp_period: 30 -otp_digits: 6 +``` +$ pass otp insert totp-secret +``` + +Pipe an `otpauth://` URI into a passfile: + +``` +$ cat totp-uri.txt | pass otp insert totp-secret +``` + +Use `[zbar](http://zbar.sourceforge.net/)` to decode a QR image into a passfile: + +``` +$ zbarimg -q --raw qrcode.png | pass otp insert totp-secret ``` Generate a 2FA code using this token: ``` -$ pass otp show totp-secret +$ pass otp totp-secret 698816 ``` +Display a QR code for an OTP token: + +``` +$ pass otp uri -q totp-secret +█████████████████████████████████████ +█████████████████████████████████████ +████ ▄▄▄▄▄ ██▄▄ ▀█ ▀ █▀█ ▄▄▄▄▄ ████ +████ █ █ █▀▄ █▀▀▄▀▀██ █ █ █ ████ +████ █▄▄▄█ █▄▀ █▄▄▄ █▀▀▄ █ █▄▄▄█ ████ +████▄▄▄▄▄▄▄█▄▀▄█ ▀ █▄█ ▀▄█▄▄▄▄▄▄▄████ +████▄▄▀██▄▄ ▀▄ █▄█▀ ▀▄▀▀▄▀█▀ ▄▀██████ +████ ▀▄▀ ▄▀ ▄▀ ▄▄ ▄ ███ ██ █ ███████ +████▀▀ ▄▄█▄▄▄▄ █ █ ▀███▀▄▀ ▀▀█ ████ +████▀▄▀ ▀ ▄█▀▄██ ▀▀▄██▀█▀▄▀▀ ▀█▀████ +████▀ █▀ ▄▄██ █▀▄▄▄ ▄▀ ▄▀ ▀ ▄▀▀████ +████ ▄ ▀█ ▄█▄ ▀ ▄██▄▀██▄ ▀▀▀█ ▄▀ ████ +████▄█▄▄▄█▄▄ █▄▄ ▀█ █▄█▀ ▄▄▄ █▄█▄████ +████ ▄▄▄▄▄ █ ▄▀▀▀▀▄ █▄▄ █▄█ ███▀████ +████ █ █ ██▀▄ █▄█ ▀█▀ ▄▄▄█▀▄ ████ +████ █▄▄▄█ █▀▄ █ █ ██▄▄▀ ▀▄█ ▄▀████ +████▄▄▄▄▄▄▄█▄█▄▄███▄█▄█▄█▄█▄██▄██████ +█████████████████████████████████████ +█████████████████████████████████████ +``` + ## Installation ``` @@ -69,6 +101,34 @@ sudo make install - `oathtool` for generating 2FA codes - `qrencode` for generating QR code images +## Migrating from pass-otp 0.1 + +`pass-otp` has switched to storing OTP tokens in the +standard +[Key Uri Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format). +You'll need to edit any saved tokens and change them to this format. For +example: + +``` +$ pass edit totp-secret +``` + +Old format: + +``` +otp_secret: AAAAAAAAAAAAAAAA +otp_type: totp +otp_algorithm: sha1 +otp_period: 30 +otp_digits: 6 +``` + +New format: + +``` +otpauth:// +``` + ## License ``` diff --git a/otp.bash b/otp.bash index 91661d8..f094fa2 100755 --- a/otp.bash +++ b/otp.bash @@ -85,16 +85,20 @@ otp_insert() { cmd_otp_usage() { cat <<-_EOF Usage: - $PROGRAM otp [show] [--clip,-c] pass-name + + $PROGRAM otp [code] [--clip,-c] pass-name 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] [uri] pass-name Insert a new OTP key URI. If one is not supplied, it will be read from stdin. Optionally, echo the input. Prompt before overwriting existing password unless forced. + $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. + $PROGRAM otp validate uri Test if the given URI is a valid OTP key URI. @@ -128,7 +132,6 @@ cmd_otp_insert() { [[ "$uri" == "$uri_again" ]] || die "Error: the entered URIs do not match." else read -r -p "Enter otpauth:// URI for $path: " -e uri - echo fi else read -r uri -- cgit v1.2.3 From 9ac7b84afb6e2f7a93d6a3ecc7dfcaa5a3c15083 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Mon, 20 Mar 2017 09:45:29 -0700 Subject: Fix commit message --- otp.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otp.bash b/otp.bash index f094fa2..df04d4f 100755 --- a/otp.bash +++ b/otp.bash @@ -143,7 +143,7 @@ cmd_otp_insert() { otp_parse_uri "$uri" - otp_insert "$path" $force "$otp_uri" "Add OTP secret for $2 to store." + otp_insert "$path" $force "$otp_uri" "Add OTP secret for $path to store." } cmd_otp_code() { -- cgit v1.2.3 From d9c681a8fbb70d40e14079e5f1a8b4aaee0b0d08 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Mon, 20 Mar 2017 09:51:27 -0700 Subject: Fill out migrating documentation --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c0ad4d..439f62e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Pipe an `otpauth://` URI into a passfile: $ cat totp-uri.txt | pass otp insert totp-secret ``` -Use `[zbar](http://zbar.sourceforge.net/)` to decode a QR image into a passfile: +Use [zbar](http://zbar.sourceforge.net/) to decode a QR image into a passfile: ``` $ zbarimg -q --raw qrcode.png | pass otp insert totp-secret @@ -126,9 +126,17 @@ otp_digits: 6 New format: ``` -otpauth:// +otpauth://totp/totp-secret?secret=AAAAAAAAAAAAAAAA&issuer=totp-secret ``` +Note that the following default values do not need to be specified in the URI: + +| parameter | default | +| --------- | ------- | +| algorithm | sha1 | +| period | 30 | +| digits | 6 | + ## License ``` -- cgit v1.2.3