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