#!/bin/sh
# sekrit
# Written in 2018-2020 by Lucas
# CC0 1.0 Universal/Public domain - No rights reserved
#
# To the extent possible under law, the author(s) have dedicated all
# copyright and related and neighboring rights to this software to the
# public domain worldwide. This software is distributed without any
# warranty. You should have received a copy of the CC0 Public Domain
# Dedication along with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.

# sekrit is a relatively small shell script for writing and reading
# encrypted files, aimed to deal mostly with accounts credentials, but
# general enough to deal with any content. It can populate each key
# with random data or with fixed data, reading either command-line
# arguments or stdin.
#
# To be used as a password manager, it's recommended to pair it with
# xclip(1). For example, for username, password and optional second
# factor:
#
#	sekrit get account/user | xclip -r -l 1 -sel clip -q
#	sekrit get account/pass | xclip -r -l 1 -sel clip -q
#	sekrit has account/2fa && sekrit get account/2fa \
#	    | $program_for_totp | xclip -r -l 1 -sel clip -q

usage()
{
	cat - <<. >&2
Usage:
	${0##*/} add key [value ...]
	${0##*/} cp [-k] key
	${0##*/} gen [-l length] [chars]
	${0##*/} get key
	${0##*/} has key
	${0##*/} ls [keys ...]

If no value was provided on command line, add reads from stdin.
.
	exit 1
}

err()
{
	printf "%s: %s\n" "${0##*/}" "$*" >&2
	exit 1
}

check_key()
{
	case $1 in
	*/ | /* | ./* | */./* | */. | ../* | */../* | */..)
		err "$1: invalid key"
		;;
	esac
}

to_number()
{
	printf "%u" "$*" 2>/dev/null
}

sekrit_add()
{
	[ $# -ge 1 ] && [ -n "$1" ] || usage
	key=$1
	check_key "$key"
	shift

	f=$SEKRIT_DIR/$key.gpg
	mkdir -p "${f%/*}"

	[ -f "$f" ] && err "key $key already exists"

	if [ $# -gt 0 ]; then
		# use all additional parameters as a single string
		printf "%s\n" "$*"
	else
		cat -
	fi | gpg2 -qae -r "$SEKRIT_GPG_ID" >"$f"
	# make it read-only
	chmod -- 400 "$f"
}

sekrit_cp()
{
	command -v xclip >/dev/null 2>&1 ||
	    err "xclip required for clipboard support"

	OPTIND=1
	rmlastnl=-rmlastnl
	while getopts k flag; do
		case "$flag" in
		k)	rmlastnl=
			;;
		*)	usage
			;;
		esac
	done
	shift $((OPTIND - 1))

	[ $# -eq 1 ] || usage
	key=$1

	sekrit_get "$key" |
	    xclip $rmlastnl -loops 1 -quiet -selection clip 2>/dev/null
}

sekrit_gen()
{
	len=43
	OPTIND=1
	while getopts l: flag; do
		case "$flag" in
		l)	len=$(to_number "$OPTARG") ||
			    err "invalid password length"
			;;
		*)	usage
			;;
		esac
	done
	shift $((OPTIND - 1))

	[ $# -le 1 ] || usage

	chars=+/0-9A-Za-z
	if [ $# -eq 1 ]; then
		[ -n "$1" ] || usage
		chars=$1
	fi

	tr -cd -- "$chars" </dev/urandom | dd bs=1 count="$len" 2>/dev/null &&
	    printf "\n"
}

sekrit_get()
{
	[ $# -eq 1 ] || usage
	key=$1
	check_key "$key"
	shift

	f=$SEKRIT_DIR/$key.gpg
	[ -f "$f" ] || err "no data for key $key"
	gpg2 -qd "$f"
}

sekrit_has()
{
	[ $# -eq 1 ] || usage
	key=$1
	check_key "$key"
	shift

	[ -f "$SEKRIT_DIR/$key.gpg" ]
}

ls_key()
{
	d=$SEKRIT_DIR$1
	find "$d" -type f -name "*.gpg" | sort |
	    sed -e "s#^$d##" -e "s#\.gpg\$##"
}

sekrit_ls()
{
	if [ $# -eq 0 ]; then
		ls_key /
	else
		for key; do
			printf "%s:\n" "$key"
			ls_key /"$key"/ | sed "s/^/  /"
			printf "\n"
		done
	fi
}

set -e

[ $# -ge 1 ] || usage
cmd=$1
shift

umask 077
: ${SEKRIT_DIR:=~/keep/sekrit}
: ${SEKRIT_GPG_ID:=myself}
mkdir -p "$SEKRIT_DIR"

case "$cmd" in
add)	sekrit_add "$@" ;;
cp)	sekrit_cp "$@" ;;
gen)	sekrit_gen "$@" ;;
get)	sekrit_get "$@" ;;
has)	sekrit_has "$@" ;;
ls)	sekrit_ls "$@" ;;
*)	usage ;;
esac
