#!/bin/bash
#
# Add, delete or search for a text line in the user's encrypted store.
#
# Requires that gpg is installed and that there is a default secret key.
#
# TODO: Localize with gettext.
#
########################################################################
#
# Copyright 2018 Bert Bos
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
########################################################################
#
# Created: 2 December 2017
# Author: Bert Bos <bert@w3.org>

set -u -e -o pipefail

USAGE="Usage: ${0##*/} -h
	 ${0##*/} [-f file] [-i] [-E|-F] [--] pattern
	 ${0##*/} -a [-f file] [--] [text]
	 ${0##*/} -m [-f file] [--] [text]
	 ${0##*/} -d [-f file] [--] text
	 ${0##*/} -e [-f file] [-i] [-E|-F] [--] pattern
	 ${0##*/} -v"
VERSION="safe 1.0"
STORE=$HOME/.safe


# help -- print usage information
function help
{
  cat <<-EOF

	  $USAGE

	  ${0##*/} manages an encrypted file of unique text records. Without -a,
	  -m, -d, and -e, it searches the file for <pattern>. Options:

	    -h  This help.
	    -a  Add <text> to the file, or prompt for text to add.
	    -m  Add <text> to the file, or prompt for multi-line text to add.
	    -d  Delete <text> from the file.
	    -e  Edit lines matching the <pattern> in a text editor.
	    -i  Search case-insensitively.
	    -F  Treat <pattern> as a literal string. (Default is basic regexp.)
	    -E  Treat <pattern> as an extended regexp.
	    -f <file>  Use <file> as encrypted file (Default is ~/.safe)
	    -v  Print version number and exit.

	  Security warnings: (1) The command line including <text> may be stored
	  unencrypted in your command history (e.g., ~/.bash_history). See your
	  shell manual for how to avoid this. (2) During editing (option -e),
	  the text being edited is stored unencrypted in a temporary file.

	EOF
}


# die -- print error message and exit
function die
{
  echo "${0##*/}: Error: $@"
  exit 1
}


# usage -- print usage message and exit
function usage
{
  echo "$USAGE" >&2
  exit 1
}


# esc -- replace newlines in argument by "\n" and "\" by "\b"
function esc
{
  local f=${*//\\/\\b}
  echo "${f//$'\n'/\\n}"
}


# expand -- unescape \b and \n, separate records visually
function expand
{
  sed '
s/$/\\n==================== DO NOT EDIT THIS LINE ====================/
s/\\n/\
/g
s/\\b/\\/g'
}


# unescape -- remove visual record separator, escape "\" and newline
function unexpand
{
  sed '
s/^==* DO NOT EDIT THIS LINE ==*$//
tprint
H
d
:print
x
s/\\/\\b/g
s/^\n//
s/\n/\\n/g'
}


# Parse command line
mode=
opts=
while getopts "f:amdeiEFhv" opt; do
  case $opt in
    f) STORE=$OPTARG;;
    a) if [ -z "$mode" ]; then mode=add; else usage;fi;;
    m) if [ -z "$mode" ]; then mode=addmulti; else usage; fi;;
    d) if [ -z "$mode" ]; then mode=delete; else usage; fi;;
    e) if [ -z "$mode" ]; then mode=edit; else usage; fi;;
    i) opts="$opts -i";;	# Case-insensitive search
    E) if [[ "$opts" =~ -F ]]; then usage; else opts="$opts -E"; fi;;
    F) if [[ "$opts" =~ -E ]]; then usage; else opts="$opts -F"; fi;;
    h) help; exit;;
    v) echo "$VERSION"; exit;;
    ?) usage;;
  esac
done
shift $((OPTIND - 1))
case ${mode:=search} in
  add|addmulti) if [ -n "$opts" ]; then usage; fi;;
  delete) if [ -n "$opts" ] || [ -z "$*" ]; then usage; fi;;
  edit|search) if [ -z "$*" ]; then usage; fi;;
esac

# If STORE doesn't exist yet, create it, with one (empty) line
[[ -f "$STORE" ]] || echo "" | gpg -q -e --default-recipient-self >"$STORE"

# Add, delete, edit or search
case $mode in

  add|addmulti)
    trap 'rm -f ${TMP:-}' 0
    TMP=$(mktemp /tmp/safe-XXXXXX)
    if [ -n "$*" ]; then f="$*"
    elif [ $mode = add ]; then echo -n "Enter data: "; read f
    else echo "End data with ^D:"; f=$(cat); fi
    f=$(esc "$f")
    cp "$STORE" "$STORE~"
    { gpg -q -d "$STORE" | grep -F -x -v -- "$f"; printf "%s\n" "$f"; } | \
      gpg -q -e --default-recipient-self >$TMP && mv $TMP "$STORE"
    ;;

  delete)
    trap 'rm -f ${TMP:-}' 0
    TMP=$(mktemp /tmp/safe-XXXXXX)
    f=$(esc "$*")
    gpg -q -d "$STORE" | grep -F -x -q -- "$f" || \
      die "Argument not found in safe."
    cp "$STORE" "$STORE~"
    gpg -q -d "$STORE~" | grep -F -x -v -- "$f" | \
      gpg -q -e --default-recipient-self >"$STORE"
    ;;

  search)
    f=$(esc "$*"); gpg -q -d $STORE | grep $opts -- "$f" | \
      sed -e 's/\\n/\'$'\n''/g' -e 's/\\b/\\/g'
    ;;

  edit)
    trap 'rm -f ${TMP1:-} ${TMP2:-}' 0
    TMP1=$(mktemp /tmp/safe-XXXXXX)
    TMP2=$(mktemp /tmp/safe-XXXXXX)
    for f in ${EDITOR:-} ${VISUAL:-}; do type -t "$f" >/dev/null && EDIT=$f; done
    [[ -n "${EDIT:-}" ]] || die "No editor found. Try setting VISUAL or EDITOR."
    f=$(esc "$*")
    gpg -q -d $STORE | grep $opts -- "$f" | expand >$TMP2
    sum=$(cksum $TMP2)
    $EDIT $TMP2
    if [ "$sum" != "$(cksum $TMP2)" ]; then
      cp "$STORE" "$STORE~"
      { gpg -q -d $STORE | grep $opts -v -- "$f"; unexpand <$TMP2; echo; } | \
    	sort -u | gpg -q -e --default-recipient-self >$TMP1 && mv $TMP1 $STORE
    fi
    ;;

esac
