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