From a4e02eabb56faa60b1c29fc4008193135a7cfd47 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Tue, 14 Feb 2017 16:13:32 -0800 Subject: Initial commit --- Makefile | 37 +++++++++ README.md | 87 +++++++++++++++++++ otp.bash | 275 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ pass-otp.1 | 130 +++++++++++++++++++++++++++++ 4 files changed, 529 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100755 otp.bash create mode 100644 pass-otp.1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..da01f52 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +PROG ?= otp +PREFIX ?= /usr +DESTDIR ?= +LIBDIR ?= $(PREFIX)/lib +SYSTEM_EXTENSION_DIR ?= $(LIBDIR)/password-store/extensions +MANDIR ?= $(PREFIX)/share/man + +all: + @echo "pass-$(PROG) is a shell script and does not need compilation, it can be simply executed." + @echo "" + @echo "To install it try \"make install\" instead." + @echo + @echo "To run pass $(PROG) one needs to have some tools installed on the system:" + @echo " password store" + +install: + @install -v -d "$(DESTDIR)$(MANDIR)/man1" && install -m 0644 -v pass-$(PROG).1 "$(DESTDIR)$(MANDIR)/man1/pass-$(PROG).1" + @install -v -d "$(DESTDIR)$(SYSTEM_EXTENSION_DIR)/" + @install -Dm0755 $(PROG).bash "$(DESTDIR)$(SYSTEM_EXTENSION_DIR)/$(PROG).bash" + @echo + @echo "pass-$(PROG) is installed succesfully" + @echo + +uninstall: + @rm -vrf \ + "$(DESTDIR)$(SYSTEM_EXTENSION_DIR)/$(PROG).bash" \ + "$(DESTDIR)$(IMPORTERS_DIR)/" \ + "$(DESTDIR)$(MANDIR)/man1/pass-$(PROG).1" \ + +test: + make -C tests + +lint: + shellcheck -s bash $(PROG).bash + + +.PHONY: install uninstall test lint diff --git a/README.md b/README.md new file mode 100644 index 0000000..72494da --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# pass-otp + +A [pass](https://www.passwordstore.org/) extension for managing +one-time-password (OTP) tokens. + +## Usage + +``` +Usage: + pass 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 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 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. +``` + +## Example + +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 show totp-secret +otp_secret: AAAAAAAAAAAAAAAAAAAAA +otp_type: totp +otp_algorithm: sha1 +otp_period: 30 +otp_digits: 6 +``` + +Generate a 2FA code using this token: + +``` +$ pass otp show totp-secret +698816 +``` + +## Installation + +```` +git clone https://github.com/tadfisher/pass-otp +cd pass-otp +sudo make install +``` + +## Requirements + +- `pass` 1.7.0 or later for extenstion support +- `oathtool` for generating 2FA codes +- `qrencode` for generating QR code images + +## License + +``` +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 . +``` diff --git a/otp.bash b/otp.bash new file mode 100755 index 0000000..00f57e1 --- /dev/null +++ b/otp.bash @@ -0,0 +1,275 @@ +#!/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 . +# [] + +OATH=$(which oathtool) + +otp_increment_counter() { + local ret=$1 + local counter=$2 contents="$3" path="$4" passfile="$5" + + local inc=$((counter+1)) + + contents=${contents//otp_counter: $counter/otp_counter: $inc} + + set_gpg_recipients "$(dirname "$path")" + + $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." + + eval $ret="'$inc'" +} + +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?" + + 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." + + git_add_file "$passfile" "Add given OTP secret for $path to store." +} + +otp_insert_totp() { + local opts 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 + + local 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_hotp() { + local opts 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" + + local contents=$(cat <<-_EOF + otp_secret: $secret + otp_type: hotp + otp_counter: $counter + otp_digits: $digits + _EOF + ) + + otp_insert "$1" $force "$contents" +} + +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 +} + +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 +} + +cmd_otp_show() { + local 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 show [--clip,-c] 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 + + local 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 + [[ $? -ne 0 ]] && 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_uri() { + local 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 + + local 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 +} + +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 "$@" ;; +esac +exit 0 diff --git a/pass-otp.1 b/pass-otp.1 new file mode 100644 index 0000000..3b69fd6 --- /dev/null +++ b/pass-otp.1 @@ -0,0 +1,130 @@ +.TH PASS-OTP 1 "2017 February 14" "Password store OTP extension" + +.SH NAME +pass-otp - A \fBpass\fP(1) extension for managing one-time-password (OTP) tokens. + +.SH SYNOPSIS +.B pass otp +[ +.I COMMAND +] [ +.I OPTIONS +]... [ +.I ARGS +]... + +.SH DESCRIPTION + +.B pass-otp +extends the +.BR pass (1) +utility with the +.B otp +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. + +.SH COMMANDS + +.TP +\fBotp show\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) +and then restore the clipboard after 45 (or \fIPASSWORD_STORE_CLIP_TIME\fP) +seconds. + +.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 +\fI--force\fP or \fI-f\fP is specified. This command is alternatively named +\fBadd totp\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 + +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 +.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 +instead display a QR code using +.BR qrencode (1) +either to the terminal or graphically if supported. + +.SH OPTIONS + +.TP +\fB\-c\fP, \fB--clip\fP +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. + +.TP +\fB\-h\fB, \-\-help\fR +Show usage message. + +.SH SEE ALSO +.BR pass(1), + + +.SH AUTHORS +.B pass-otp +was written by +.MT tadfisher@gmail.com +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 +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 . -- cgit v1.2.3