aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--README.md118
-rwxr-xr-xotp.bash439
-rw-r--r--pass-otp.182
-rwxr-xr-xtest/code.t27
-rwxr-xr-xtest/insert.t87
-rw-r--r--test/setup.sh14
-rwxr-xr-xtest/uri.t23
-rwxr-xr-xtest/validate.t31
9 files changed, 505 insertions, 318 deletions
diff --git a/.travis.yml b/.travis.yml
index ed6b706..4122f71 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,8 @@ addons:
sources:
- debian-sid
packages:
+ - expect
+ - oathtool
- shellcheck
before_script:
diff --git a/README.md b/README.md
index ad3a121..439f62e 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,42 @@ 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://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
```
diff --git a/otp.bash b/otp.bash
index 4a21322..df04d4f 100755
--- a/otp.bash
+++ b/otp.bash
@@ -18,258 +18,239 @@
OATH=$(which oathtool)
-otp_increment_counter() {
- local ret=$1
- local counter=$2 contents="$3" path="$4" passfile="$5"
+# 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="$1"
+
+ 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]}
+ [[ -z $otp_accountname ]] && die "Invalid key URI (missing accountname): $otp_uri"
+
+ 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[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"
+
+ pattern='^[0-9]+$'
+ [[ "$otp_type" == 'hotp' ]] && [[ ! "$otp_counter" =~ $pattern ]] && die "Invalid key URI (missing counter): $otp_uri"
+}
- local inc=$((counter+1))
+otp_insert() {
+ local path="${1%/}"
+ local passfile="$PREFIX/$path.gpg"
+ local force=$2
+ local contents="$3"
+ local message="$4"
- contents=${contents//otp_counter: $counter/otp_counter: $inc}
+ check_sneaky_paths "$path"
+ set_git "$passfile"
- set_gpg_recipients "$(dirname "$path")"
+ [[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
- $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted."
+ mkdir -p -v "$PREFIX/$(dirname "$path")"
+ set_gpg_recipients "$(dirname "$path")"
- git_add_file "$passfile" "Update HOTP counter value for $path."
+ $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted."
- eval "$ret='$inc'"
+ git_add_file "$passfile" "$message"
}
-otp_insert() {
- local path="${1%/}"
- local passfile="$PREFIX/$path.gpg"
- local force=$2
- local contents="$3"
-
- check_sneaky_paths "$path"
-
- [[ $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")"
+cmd_otp_usage() {
+ cat <<-_EOF
+Usage:
- $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted."
+ $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.
- git_add_file "$passfile" "Add given OTP secret for $path to store."
-}
+ $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.
-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"
-}
+ $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.
-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"
-}
+ $PROGRAM otp validate uri
+ Test if the given URI is a valid OTP key 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
+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 "$@" ;;
- *) die "Invalid OTP type '$1'. May be one of 'totp' or 'hotp'" ;;
- esac
+ 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 && $# -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
+ 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
+ fi
+ else
+ read -r uri
+ fi
+ else
+ uri="$1"
+ path="$2"
+ fi
+
+ otp_parse_uri "$uri"
+
+ otp_insert "$path" $force "$otp_uri" "Add OTP secret for $path to store."
}
-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 \
- || 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
+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=$?
+ 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 [--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
+ 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=$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=$otp_digits"
+ cmd+=" $otp_secret"
+ ;;
+ 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
+ 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"
+
+ contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile")
+ while read -r -a line; do
+ if [[ "$line" == otpauth://* ]]; then
+ otp_parse_uri "$line"
+ break
+ fi
+ done <<< "$contents"
+
+ if [[ clip -eq 1 ]]; then
+ clip "$otp_uri" "OTP key URI for $path"
+ elif [[ qrcode -eq 1 ]]; then
+ qrcode "$otp_uri" "OTP key URI for $path"
+ else
+ echo "$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 "$@" ;;
- *) cmd_otp_show "$@" ;;
+ help|--help|-h) shift; cmd_otp_usage "$@" ;;
+ insert|add) shift; cmd_otp_insert "$@" ;;
+ uri) shift; cmd_otp_uri "$@" ;;
+ validate) shift; cmd_otp_validate "$@" ;;
+ code|show) shift; cmd_otp_code "$@" ;;
+ *) cmd_otp_code "$@" ;;
esac
exit 0
diff --git a/pass-otp.1 b/pass-otp.1
index 3b69fd6..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.
@@ -23,45 +23,39 @@ 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)
+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.
+seconds. This command is alternatively named \fBshow\fP.
.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
-
-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
+\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 .
+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 totp\fP.
+\fBadd\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 uri\fP [ \fI--clip\fP, \fI-c\fP | \fI--qrcode\fP, \fI-q\fP ] \fIpass-name\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
@@ -69,6 +63,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
@@ -77,30 +79,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/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
diff --git a/test/insert.t b/test/insert.t
index a78971f..b4af4f5 100755
--- a/test/insert.t
+++ b/test/insert.t
@@ -4,16 +4,87 @@ export test_description="Tests pass otp insert commands"
. ./setup.sh
-test_expect_success 'Inserts a basic TOTP key' '
- "$PASS" init $KEY1 &&
- "$PASS" otp insert totp -s AAAAAAAAAAAAAAAAAAAAA totp-secret
+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
+ expect <<EOD
+ spawn "$PASS" otp insert "$uri2" passfile
+ expect {
+ "An entry already exists" {
+ send "n\r"
+ exp_continue
+ }
+ eof
+ }
+EOD
+ [[ $("$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 '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_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
+ expect <<EOD
+ spawn "$PASS" otp insert passfile
+ expect {
+ "Enter" {
+ send "$uri\r"
+ exp_continue
+ }
+ "Retype" {
+ send "$uri\r"
+ exp_continue
+ }
+ eof
+ }
+EOD
+ [[ $("$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
+ expect <<EOD
+ spawn "$PASS" otp insert -e passfile
+ expect {
+ "Enter" {
+ send "$uri\r"
+ exp_continue
+ }
+ eof
+ }
+EOD
+ [[ $("$PASS" show passfile) == "$uri" ]]
'
test_done
diff --git a/test/setup.sh b/test/setup.sh
index f896382..f407164 100644
--- a/test/setup.sh
+++ b/test/setup.sh
@@ -1,3 +1,5 @@
+#!/usr/bin/env bash
+
# This file should be sourced by all test-scripts
#
# This scripts sets the following:
@@ -33,12 +35,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 +61,9 @@ 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"
+}
diff --git a/test/uri.t b/test/uri.t
new file mode 100755
index 0000000..084b010
--- /dev/null
+++ b/test/uri.t
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+export test_description='Tests pass otp URI parsing'
+
+. ./setup.sh
+
+test_expect_success 'Shows key URI in single-line passfile' '
+ uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"
+
+ test_pass_init &&
+ "$PASS" otp insert "$uri" passfile &&
+ [[ $("$PASS" otp uri passfile) == "$uri" ]]
+'
+
+test_expect_success 'Shows key URI in multi-line passfile' '
+ uri="otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"
+
+ 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..6d05fdf
--- /dev/null
+++ b/test/validate.t
@@ -0,0 +1,31 @@
+#!/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 '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/
+'
+
+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