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