pass-otp/otp.bash

264 lines
7.5 KiB
Bash
Raw Normal View History

2017-02-15 01:13:32 +01:00
#!/usr/bin/env bash
# pass otp - Password Store Extension (https://www.passwordstore.org/)
# Copyright (C) 2017 Tad Fisher
#
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# []
OATH=$(which oathtool)
2017-03-18 23:37:07 +01:00
# 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() {
2017-03-19 02:20:28 +01:00
local uri="$1"
2017-03-18 23:37:07 +01:00
uri="${uri//\`/%60}"
uri="${uri//\"/%22}"
2017-03-20 04:58:45 +01:00
local pattern='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?))?\?(.+)$'
2017-03-18 23:37:07 +01:00
[[ "$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"
2017-03-18 23:37:07 +01:00
2017-03-20 04:58:45 +01:00
local p=${BASH_REMATCH[7]}
local IFS=\&; local params=(${p[@]}); unset IFS
pattern='^(.+)=(.+)$'
for param in "${params[@]}"; do
2017-03-18 23:37:07 +01:00
if [[ "$param" =~ $pattern ]]; then
case ${BASH_REMATCH[1]} in
2017-03-20 04:58:45 +01:00
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]} ;;
2017-03-18 23:37:07 +01:00
*) ;;
esac
fi
done
[[ -z "$otp_secret" ]] && die "Invalid key URI (missing secret): $otp_uri"
2017-03-20 04:58:45 +01:00
pattern='^[0-9]+$'
[[ "$otp_type" == 'hotp' ]] && [[ ! "$otp_counter" =~ $pattern ]] && die "Invalid key URI (missing counter): $otp_uri"
2017-03-18 23:37:07 +01:00
}
2017-02-15 01:13:32 +01:00
otp_insert() {
local path="${1%/}"
local passfile="$PREFIX/$path.gpg"
local force=$2
local contents="$3"
2017-03-20 05:00:12 +01:00
local message="$4"
2017-02-15 01:13:32 +01:00
check_sneaky_paths "$path"
set_git "$passfile"
2017-02-15 01:13:32 +01:00
[[ $force -eq 0 && -e $passfile ]] && yesno "An entry already exists for $path. Overwrite it?"
2017-02-15 01:13:32 +01:00
mkdir -p -v "$PREFIX/$(dirname "$path")"
set_gpg_recipients "$(dirname "$path")"
2017-02-15 01:13:32 +01:00
$GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$contents" || die "OTP secret encryption aborted."
2017-02-15 01:13:32 +01:00
2017-03-20 05:00:12 +01:00
git_add_file "$passfile" "$message"
2017-02-15 01:13:32 +01:00
}
cmd_otp_usage() {
cat <<-_EOF
Usage:
2017-03-20 17:37:35 +01:00
$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.
2017-03-20 17:37:35 +01:00
$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.
2017-03-20 17:37:35 +01:00
$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.
2017-03-20 17:37:35 +01:00
$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
exit 0
2017-02-15 01:13:32 +01:00
}
cmd_otp_insert() {
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 ]] && die "Usage: $PROGRAM $COMMAND insert [--force,-f] [pass-name]"
local prompt path uri
if [[ $# -eq 1 ]]; then
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"
if [[ -z "$path" ]]; then
[[ -n "$otp_issuer" ]] && path+="$otp_issuer/"
path+="$otp_accountname"
yesno "Insert into $path?"
fi
2017-03-20 17:45:29 +01:00
otp_insert "$path" $force "$otp_uri" "Add OTP secret for $path to store."
2017-02-15 01:13:32 +01:00
}
2017-03-20 05:00:12 +01:00
cmd_otp_code() {
[[ -z "$OATH" ]] && die "Failed to generate OTP code: oathtool is not installed."
2017-03-20 05:00:12 +01:00
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
2017-03-20 05:00:12 +01:00
[[ $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")
2017-03-20 05:00:12 +01:00
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"
2017-03-20 05:00:12 +01:00
cmd+=" $otp_secret"
;;
hotp)
local counter=$((otp_counter+1))
cmd="$OATH -b --hotp --counter=$counter"
[[ -n "$otp_digits" ]] && cmd+=" --digits=$otp_digits"
2017-03-20 05:00:12 +01:00
cmd+=" $otp_secret"
;;
esac
2017-03-20 05:00:12 +01:00
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
2017-02-15 01:13:32 +01:00
}
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"
contents=$($GPG -d "${GPG_OPTS[@]}" "$passfile")
while read -r -a line; do
2017-03-20 05:00:12 +01:00
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
2017-02-15 01:13:32 +01:00
}
2017-03-18 23:37:07 +01:00
cmd_otp_validate() {
otp_parse_uri "$1"
2017-03-18 23:37:07 +01:00
}
2017-02-15 01:13:32 +01:00
case "$1" in
help|--help|-h) shift; cmd_otp_usage "$@" ;;
insert|add) shift; cmd_otp_insert "$@" ;;
uri) shift; cmd_otp_uri "$@" ;;
validate) shift; cmd_otp_validate "$@" ;;
2017-03-20 05:00:12 +01:00
code|show) shift; cmd_otp_code "$@" ;;
*) cmd_otp_code "$@" ;;
2017-02-15 01:13:32 +01:00
esac
exit 0