#!/usr/bin/env nix-shell
#! nix-shell -i bash --pure
#! nix-shell -p bash openssl git unixtools.column
set -euo pipefail

#
# transcrypt - https://github.com/elasticdog/transcrypt
#
# A script to configure transparent encryption of sensitive files stored in
# a Git repository. It utilizes OpenSSL's symmetric cipher routines and follows
# the gitattributes(5) man page regarding the use of filters.
#
# Copyright (c) 2014-2019 Aaron Bull Schaefer <aaron@elasticdog.com>
# This source code is provided under the terms of the MIT License
# that can be be found in the LICENSE file.
#

##### CONSTANTS

# the release version of this script
readonly VERSION='2.0.0'

# the default cipher to utilize
readonly DEFAULT_CIPHER='aes-256-ctr'

# the openssl options to encrypt/decrypt the files
# shellcheck disable=SC2016
readonly ENCRYPT_OPTIONS='-$cipher -pbkdf2 -iter 200000'

# regular expression used to test user input
readonly YES_REGEX='^[Yy]$'

## Repository Metadata

# whether or not transcrypt is already configured
readonly CONFIGURED=$(git config --get --local transcrypt.version 2>/dev/null)

# the current git repository's top-level directory
readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null)

# whether or not a HEAD revision exists
readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null)

# https://github.com/RichiH/vcsh
# whether or not the git repository is running under vcsh
readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null)

# whether or not the git repository is bare
readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null)

## Git Directory Handling

# print a canonicalized absolute pathname
realpath() {
	local path=$1

	# make path absolute
	local abspath=$path
	if [[ -n ${abspath##/*} ]]; then
		abspath=$(pwd -P)/$abspath
	fi

	# canonicalize path
	local dirname=
	if [[ -d $abspath ]]; then
		dirname=$(cd "$abspath" && pwd -P)
		abspath=$dirname
	elif [[ -e $abspath ]]; then
		dirname=$(cd "${abspath%/*}/" 2>/dev/null && pwd -P)
		abspath=$dirname/${abspath##*/}
	fi

	if [[ -d $dirname && -e $abspath ]]; then
		printf '%s\n' "$abspath"
	else
		printf 'invalid path: %s\n' "$path" >&2
		exit 1
	fi
}

# the current git repository's .git directory
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null)

# the current git repository's gitattributes file
readonly CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile)
if [[ $CORE_ATTRIBUTES ]]; then
	readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
	readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes"
else
	readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
fi

##### FUNCTIONS

# print a message to stderr
warn() {
	local fmt="$1"
	shift
	# shellcheck disable=SC2059
	printf "transcrypt: $fmt\n" "$@" >&2
}

# print a message to stderr and exit with either
# the given status or that of the most recent command
die() {
	local st="$?"
	if [[ "$1" != *[^0-9]* ]]; then
		st="$1"
		shift
	fi
	warn "$@"
	exit "$st"
}

# verify that all requirements have been met
run_safety_checks() {
	# validate that we're in a git repository
	[[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?'

	# exit if transcrypt is not in the required state
	if [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then
		die 1 'the current repository is not configured'
	elif [[ ! $requires_existing_config ]] && [[ $CONFIGURED ]]; then
		die 1 'the current repository is already configured; see --display'
	fi

	# check for dependencies
	for cmd in {column,grep,mktemp,openssl,sed,tee}; do
		command -v $cmd >/dev/null || die 'required command "%s" was not found' "$cmd"
	done

	# ensure the repository is clean (if it has a HEAD revision) so we can force
	# checkout files without the destruction of uncommitted changes
	if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
		# check if the repo is dirty
		if ! git diff-index --quiet HEAD --; then
			die 1 'the repo is dirty; commit or stash your changes before running transcrypt'
		fi
	fi
}

# unset the cipher variable if it is not supported by openssl
validate_cipher() {
  local list_cipher_commands
  list_cipher_commands='openssl enc -ciphers'
  remove_dash() {
    sed 's#\(^\| \)-#\1#g'
  }


	local supported
	supported=$($list_cipher_commands | remove_dash | tr -s ' ' '\n' | grep --line-regexp "$cipher") || true
	if [[ ! $supported ]]; then
		if [[ $interactive ]]; then
			printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher"
			$list_cipher_commands | remove_dash | column -c 80
			printf '\n'
			cipher=''
		else
			# shellcheck disable=SC2016
			die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$($list_cipher_commands | remove_dash)"
		fi
	fi
}

# ensure we have a cipher to encrypt with
get_cipher() {
	while [[ ! $cipher ]]; do
		local answer=
		if [[ $interactive ]]; then
			printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER"
			read -r answer
		fi

		# use the default cipher if the user gave no answer;
		# otherwise verify the given cipher is supported by openssl
		if [[ ! $answer ]]; then
			cipher=$DEFAULT_CIPHER
		else
			cipher=$answer
			validate_cipher
		fi
	done
}

# ensure we have a password to encrypt with
get_password() {
	while [[ ! $password ]]; do
		local answer=
		if [[ $interactive ]]; then
			printf 'Generate a random password? [Y/n] '
			read -r -n 1 -s answer
			printf '\n'
		fi

		# generate a random password if the user answered yes;
		# otherwise prompt the user for a password
		if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
			local password_length=30
			local random_base64
			random_base64=$(openssl rand -base64 $password_length)
			password=$random_base64
		else
			printf 'Password: '
			read -r password
			[[ $password ]] || printf 'no password was specified\n'
		fi
	done
}

# confirm the transcrypt configuration
confirm_configuration() {
	local answer=

	printf '\nRepository metadata:\n\n'
	[[ ! $REPO ]] || printf '  GIT_WORK_TREE:  %s\n' "$REPO"
	printf '  GIT_DIR:        %s\n' "$GIT_DIR"
	printf '  GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
	printf 'The following configuration will be saved:\n\n'
	printf '  CIPHER:   %s\n' "$cipher"
	printf '  PASSWORD: %s\n\n' "$password"
	printf 'Does this look correct? [Y/n] '
	read -r -n 1 -s answer

	# exit if the user did not confirm
	if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
		printf '\n\n'
	else
		printf '\n'
		die 1 'configuration has been aborted'
	fi
}

# confirm the rekey configuration
confirm_rekey() {
	local answer=

	printf '\nRepository metadata:\n\n'
	[[ ! $REPO ]] || printf '  GIT_WORK_TREE:  %s\n' "$REPO"
	printf '  GIT_DIR:        %s\n' "$GIT_DIR"
	printf '  GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
	printf 'The following configuration will be saved:\n\n'
	printf '  CIPHER:   %s\n' "$cipher"
	printf '  PASSWORD: %s\n\n' "$password"
	printf 'You are about to re-encrypt all encrypted files using new credentials.\n'
	printf 'Once you do this, their historical diffs will no longer display in plain text.\n\n'
	printf 'Proceed with rekey? [y/N] '
	read -r answer

	# only rekey if the user explicitly confirmed
	if [[ $answer =~ $YES_REGEX ]]; then
		printf '\n'
	else
		die 1 'rekeying has been aborted'
	fi
}

# automatically stage rekeyed files in preparation for the user to commit them
stage_rekeyed_files() {
	local encrypted_files
	encrypted_files=$(git ls-crypt)
	if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then
		# touch all encrypted files to prevent stale stat info
		cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
		# shellcheck disable=SC2086
		touch $encrypted_files
		# shellcheck disable=SC2086
		git update-index --add -- $encrypted_files

		printf '***  rekeyed files have been staged  ***\n'
		printf '*** COMMIT THESE CHANGES RIGHT AWAY! ***\n\n'
	fi
}

# save helper scripts under the repository's git directory
save_helper_scripts() {
	mkdir -p "${GIT_DIR}/crypt"

  openssl_command="openssl enc $ENCRYPT_OPTIONS -pass env:ENC_PASS"

	# The `decryption -> encryption` process on an unchanged file must be
	# deterministic for everything to work transparently. To do that, the same
	# salt must be used each time we encrypt the same file. An HMAC has been
	# proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file
	# (keyed with a combination of the filename and transcrypt password), and
	# then use the last 16 bytes of that HMAC for the file's unique salt.

	cat <<-'EOF' >"${GIT_DIR}/crypt/clean"
		#!/usr/bin/env bash
		filename=$1
		# ignore empty files
		if [[ -s $filename ]]; then
		  # cache STDIN to test if it's already encrypted
		  tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
		  trap 'rm -f "$tempfile"' EXIT
		  tee "$tempfile" &>/dev/null
		  # the first bytes of an encrypted file are always "Salted" in Base64
		  read -n 8 firstbytes <"$tempfile"
		  if [[ $firstbytes == "U2FsdGVk" ]]; then
		    cat "$tempfile"
		  else
		    cipher=$(git config --get --local transcrypt.cipher)
		    password=$(git config --get --local transcrypt.password)
		    salt=$(openssl dgst -hmac "${filename}:${password}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16)
		    ENC_PASS=$password @openssl_command@ -e -a -S "$salt" -in "$tempfile"
		  fi
		fi
	EOF

	cat <<-'EOF' >"${GIT_DIR}/crypt/smudge"
		#!/usr/bin/env bash
		tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
		trap 'rm -f "$tempfile"' EXIT
		cipher=$(git config --get --local transcrypt.cipher)
		password=$(git config --get --local transcrypt.password)
		tee "$tempfile" | ENC_PASS=$password @openssl_command@ -d -a 2>/dev/null || cat "$tempfile"
	EOF

	cat <<-'EOF' >"${GIT_DIR}/crypt/textconv"
		#!/usr/bin/env bash
		filename=$1
		# ignore empty files
		if [[ -s $filename ]]; then
		  cipher=$(git config --get --local transcrypt.cipher)
		  password=$(git config --get --local transcrypt.password)
		  ENC_PASS=$password @openssl_command@ -d -a -in "$filename" 2>/dev/null || cat "$filename"
		fi
	EOF

	# make scripts executable
	for script in {clean,smudge,textconv}; do
		chmod 0755 "${GIT_DIR}/crypt/${script}"
    sed "s/@openssl_command@/$openssl_command/" -i "${GIT_DIR}/crypt/${script}"
	done
}

# write the configuration to the repository's git config
save_configuration() {
	save_helper_scripts

	# write the encryption info
	git config transcrypt.version "$VERSION"
	git config transcrypt.cipher "$cipher"
	git config transcrypt.password "$password"

	# write the filter settings
	if [[ -d $(git rev-parse --git-common-dir) ]]; then
		# this allows us to support multiple working trees via git-worktree
		# ...but the --git-common-dir flag was only added in November 2014
		# shellcheck disable=SC2016
		git config filter.crypt.clean '"$(git rev-parse --git-common-dir)"/crypt/clean %f'
		# shellcheck disable=SC2016
		git config filter.crypt.smudge '"$(git rev-parse --git-common-dir)"/crypt/smudge'
		# shellcheck disable=SC2016
		git config diff.crypt.textconv '"$(git rev-parse --git-common-dir)"/crypt/textconv'
	else
		# shellcheck disable=SC2016
		git config filter.crypt.clean '"$(git rev-parse --git-dir)"/crypt/clean %f'
		# shellcheck disable=SC2016
		git config filter.crypt.smudge '"$(git rev-parse --git-dir)"/crypt/smudge'
		# shellcheck disable=SC2016
		git config diff.crypt.textconv '"$(git rev-parse --git-dir)"/crypt/textconv'
	fi
	git config filter.crypt.required 'true'
	git config diff.crypt.cachetextconv 'true'
	git config diff.crypt.binary 'true'
	git config merge.renormalize 'true'

	# add a git alias for listing encrypted files
	git config alias.ls-crypt "!git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'"
}

# display the current configuration settings
display_configuration() {
	local current_cipher
	current_cipher=$(git config --get --local transcrypt.cipher)
	local current_password
	current_password=$(git config --get --local transcrypt.password)
	local escaped_password=${current_password//\'/\'\\\'\'}

	printf 'The current repository was configured using transcrypt version %s\n' "$CONFIGURED"
	printf 'and has the following configuration:\n\n'
	[[ ! $REPO ]] || printf '  GIT_WORK_TREE:  %s\n' "$REPO"
	printf '  GIT_DIR:        %s\n' "$GIT_DIR"
	printf '  GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
	printf '  CIPHER:   %s\n' "$current_cipher"
	printf '  PASSWORD: %s\n\n' "$current_password"
	printf 'Copy and paste the following command to initialize a cloned repository:\n\n'
	printf "  transcrypt -c %s -p '%s'\n" "$current_cipher" "$escaped_password"
}

# remove transcrypt-related settings from the repository's git config
clean_gitconfig() {
	git config --remove-section transcrypt 2>/dev/null || true
	git config --remove-section filter.crypt 2>/dev/null || true
	git config --remove-section diff.crypt 2>/dev/null || true
	git config --unset merge.renormalize

	# remove the merge section if it's now empty
	local merge_values
	merge_values=$(git config --get-regex --local 'merge\..*') || true
	if [[ ! $merge_values ]]; then
		git config --remove-section merge 2>/dev/null || true
	fi
}

# force the checkout of any files with the crypt filter applied to them;
# this will decrypt existing encrypted files if you've just cloned a repository,
# or it will encrypt locally decrypted files if you've just flushed the credentials
force_checkout() {
	# make sure a HEAD revision exists
	if [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
		# this would normally delete uncommitted changes in the working directory,
		# but we already made sure the repo was clean during the safety checks
		local encrypted_files
		encrypted_files=$(git ls-crypt)
		cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
		IFS=$'\n'
		for file in $encrypted_files; do
			rm "$file"
			git checkout --force HEAD -- "$file" >/dev/null
		done
		unset IFS
	fi
}

# remove the locally cached encryption credentials and
# re-encrypt any files that had been previously decrypted
flush_credentials() {
	local answer=

	if [[ $interactive ]]; then
		printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n'
		printf 'All previously decrypted files will revert to their encrypted form.\n\n'
		printf 'Proceed with credential flush? [y/N] '
		read -r answer
		printf '\n'
	else
		# although destructive, we should support the --yes option
		answer='y'
	fi

	# only flush if the user explicitly confirmed
	if [[ $answer =~ $YES_REGEX ]]; then
		clean_gitconfig

		# re-encrypt any files that had been previously decrypted
		force_checkout

		printf 'The local transcrypt credentials have been successfully flushed.\n'
	else
		die 1 'flushing of credentials has been aborted'
	fi
}

# remove all transcrypt configuration from the repository
uninstall_transcrypt() {
	local answer=

	if [[ $interactive ]]; then
		printf 'You are about to remove all transcrypt configuration from your repository.\n'
		printf 'All previously encrypted files will remain decrypted in this working copy.\n\n'
		printf 'Proceed with uninstall? [y/N] '
		read -r answer
		printf '\n'
	else
		# although destructive, we should support the --yes option
		answer='y'
	fi

	# only uninstall if the user explicitly confirmed
	if [[ $answer =~ $YES_REGEX ]]; then
		clean_gitconfig

		# remove helper scripts
		for script in {clean,smudge,textconv}; do
			[[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}"
		done
		[[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt"

		# touch all encrypted files to prevent stale stat info
		local encrypted_files
		encrypted_files=$(git ls-crypt)
		if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then
			cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
			# shellcheck disable=SC2086
			touch $encrypted_files
		fi

		# remove the `git ls-crypt` alias
		git config --unset alias.ls-crypt

		# remove the alias section if it's now empty
		local alias_values
		alias_values=$(git config --get-regex --local 'alias\..*') || true
		if [[ ! $alias_values ]]; then
			git config --remove-section alias 2>/dev/null || true
		fi

		# remove any defined crypt patterns in gitattributes
		case $OSTYPE in
		darwin*)
			/usr/bin/sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
			;;
		linux*)
			sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
			;;
		esac

		printf 'The transcrypt configuration has been completely removed from the repository.\n'
	else
		die 1 'uninstallation has been aborted'
	fi
}

# list all of the currently encrypted files in the repository
list_files() {
	if [[ $IS_BARE == 'false' ]]; then
		cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
		git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'
	fi
}

# show the raw file as stored in the git commit object
show_raw_file() {
	if [[ -f $show_file ]]; then
		# ensure the file is currently being tracked
		local escaped_file=${show_file//\//\\\/}
		if git ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then
			file_paths=$(git ls-tree --name-only --full-name HEAD "$show_file")
		else
			die 1 'the file "%s" is not currently being tracked by git' "$show_file"
		fi
	elif [[ $show_file == '*' ]]; then
		file_paths=$(git ls-crypt)
	else
		die 1 'the file "%s" does not exist' "$show_file"
	fi

	IFS=$'\n'
	for file in $file_paths; do
		printf '==> %s <==\n' "$file" >&2
		git --no-pager show HEAD:"$file" --no-textconv
		printf '\n' >&2
	done
	unset IFS
}

# export password and cipher to a gpg encrypted file
export_gpg() {
	# check for dependencies
	command -v gpg >/dev/null || die 'required command "gpg" was not found'

	# ensure the recipient key exists
	if ! gpg --list-keys "$gpg_recipient" 2>/dev/null; then
		die 1 'GPG recipient key "%s" does not exist' "$gpg_recipient"
	fi

	local current_cipher
	current_cipher=$(git config --get --local transcrypt.cipher)
	local current_password
	current_password=$(git config --get --local transcrypt.password)
	mkdir -p "${GIT_DIR}/crypt"

	local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -"
	printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${GIT_DIR}/crypt/${gpg_recipient}.asc"
	printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient"
}

# import password and cipher from a gpg encrypted file
import_gpg() {
	# check for dependencies
	command -v gpg >/dev/null || die 'required command "gpg" was not found'

	local path
	if [[ -f "${GIT_DIR}/crypt/${gpg_import_file}" ]]; then
		path="${GIT_DIR}/crypt/${gpg_import_file}"
	elif [[ -f "${GIT_DIR}/crypt/${gpg_import_file}.asc" ]]; then
		path="${GIT_DIR}/crypt/${gpg_import_file}.asc"
	elif [[ ! -f $gpg_import_file ]]; then
		die 1 'the file "%s" does not exist' "$gpg_import_file"
	else
		path="$gpg_import_file"
	fi

	local configuration=''
	local safety_counter=0 # fix for intermittent 'no secret key' decryption failures
	while [[ ! $configuration ]]; do
		configuration=$(gpg --batch --quiet --decrypt "$path")

		safety_counter=$((safety_counter + 1))
		if [[ $safety_counter -eq 3 ]]; then
			die 1 'unable to decrypt the file "%s"' "$path"
		fi
	done

	cipher=$(printf '%s' "$configuration" | grep '^cipher' | cut -d'=' -f 2-)
	password=$(printf '%s' "$configuration" | grep '^password' | cut -d'=' -f 2-)
}

# print this script's usage message to stderr
usage() {
	cat <<-EOF >&2
		usage: transcrypt [-c CIPHER] [-p PASSWORD] [-h]
	EOF
}

# print this script's help message to stdout
help() {
	cat <<-EOF
		NAME
		     transcrypt -- transparently encrypt files within a git repository

		SYNOPSIS
		     transcrypt [options...]

		DESCRIPTION

		     transcrypt  will  configure a Git repository to support the transparent
		     encryption/decryption of files by utilizing OpenSSL's symmetric  cipher
		     routines  and  Git's  built-in clean/smudge filters. It will also add a
		     Git alias "ls-crypt" to list all transparently encrypted  files  within
		     the repository.

		     The  transcrypt  source  code  and full documentation may be downloaded
		     from https://github.com/elasticdog/transcrypt.

		OPTIONS
		     -c, --cipher=CIPHER
		            the symmetric cipher to utilize for encryption;
		            defaults to aes-256-cbc

		     -p, --password=PASSWORD
		            the password to derive the key from;
		            defaults to 30 random base64 characters

		     -y, --yes
		            assume yes and accept defaults for non-specified options

		     -d, --display
		            display the current repository's cipher and password

		     -r, --rekey
		            re-encrypt all encrypted files using new credentials

		     -f, --flush-credentials
		            remove the locally cached encryption credentials and  re-encrypt
		            any files that had been previously decrypted

		     -F, --force
		            ignore whether the git directory is clean, proceed with the
		            possibility that uncommitted changes are overwritten

		     -u, --uninstall
		            remove  all  transcrypt  configuration  from  the repository and
		            leave files in the current working copy decrypted

		     -l, --list
		            list all of the transparently encrypted files in the repository,
		            relative to the top-level directory

		     -s, --show-raw=FILE
		            show  the  raw file as stored in the git commit object; use this
		            to check if files are encrypted as expected

		     -e, --export-gpg=RECIPIENT
		            export  the  repository's cipher and password to a file encrypted
		            for a gpg recipient

		     -i, --import-gpg=FILE
		            import the password and cipher from a gpg encrypted file

		     -v, --version
		            print the version information

		     -h, --help
		            view this help message

		EXAMPLES

		     To initialize a Git repository to support transparent encryption,  just
		     change  into  the  repo  and run the transcrypt script. transcrypt will
		     prompt you interactively for all required  information  if  the  corre-
		     sponding option flags were not given.

		         $ cd <path-to-your-repo>/
		         $ transcrypt

		     Once  a  repository has been configured with transcrypt, you can trans-
		     parently encrypt files by applying the "crypt" filter  and  diff  to  a
		     pattern in the top-level .gitattributes config. If that pattern matches
		     a file in your repository, the file  will  be  transparently  encrypted
		     once you stage and commit it:

		         $ echo 'sensitive_file  filter=crypt diff=crypt' >> .gitattributes
		         $ git add .gitattributes sensitive_file
		         $ git commit -m 'Add encrypted version of a sensitive file'

		     See the gitattributes(5) man page for more information.

		     If  you  have  just  cloned  a  repository  containing  files  that are
		     encrypted, you'll want to configure transcrypt with the same cipher and
		     password  as  the  origin  repository.  Once  transcrypt has stored the
		     matching  credentials,  it  will  force  a  checkout  of  any  existing
		     encrypted files in order to decrypt them.

		     If  the  origin  repository  has  just rekeyed, all clones should flush
		     their transcrypt credentials, fetch and merge the new  encrypted  files
		     via Git, and then re-configure transcrypt with the new credentials.

		AUTHOR
		     Aaron Bull Schaefer <aaron@elasticdog.com>

		SEE ALSO
		     enc(1), gitattributes(5)
	EOF
}

##### MAIN

# reset all variables that might be set
cipher=''
password=''
interactive='true'
display_config=''
rekey=''
flush_creds=''
uninstall=''
show_file=''
gpg_recipient=''
gpg_import_file=''

# used to bypass certain safety checks
requires_existing_config=''
requires_clean_repo='true'

# parse command line options
while [[ "${1:-}" != '' ]]; do
	case $1 in
	-c | --cipher)
		cipher=$2
		shift
		;;
	--cipher=*)
		cipher=${1#*=}
		;;
	-p | --password)
		password=$2
		shift
		;;
	--password=*)
		password=${1#*=}
		;;
	-y | --yes)
		interactive=''
		;;
	-d | --display)
		display_config='true'
		requires_existing_config='true'
		requires_clean_repo=''
		;;
	-r | --rekey)
		rekey='true'
		requires_existing_config='true'
		;;
	-f | --flush-credentials)
		flush_creds='true'
		requires_existing_config='true'
		;;
	-F | --force)
		requires_clean_repo=''
		;;
	-u | --uninstall)
		uninstall='true'
		requires_existing_config='true'
		requires_clean_repo=''
		;;
	-l | --list)
		list_files
		exit 0
		;;
	-s | --show-raw)
		show_file=$2
		show_raw_file
		exit 0
		;;
	--show-raw=*)
		show_file=${1#*=}
		show_raw_file
		exit 0
		;;
	-e | --export-gpg)
		gpg_recipient=$2
		requires_existing_config='true'
		requires_clean_repo=''
		shift
		;;
	--export-gpg=*)
		gpg_recipient=${1#*=}
		requires_existing_config='true'
		requires_clean_repo=''
		;;
	-i | --import-gpg)
		gpg_import_file=$2
		shift
		;;
	--import-gpg=*)
		gpg_import_file=${1#*=}
		;;
	-v | --version)
		printf 'transcrypt %s\n' "$VERSION"
		exit 0
		;;
	-h | --help | -\?)
		help
		exit 0
		;;
	--*)
		warn 'unknown option -- %s' "${1#--}"
		usage
		exit 1
		;;
	*)
		warn 'unknown option -- %s' "${1#-}"
		usage
		exit 1
		;;
	esac
	shift
done

# always run our safety checks
run_safety_checks

# in order to keep behavior consistent no matter what order the options were
# specified in, we must run these here rather than in the case statement above
if [[ $uninstall ]]; then
	uninstall_transcrypt
	exit 0
elif [[ $display_config ]] && [[ $flush_creds ]]; then
	display_configuration
	printf '\n'
	flush_credentials
	exit 0
elif [[ $display_config ]]; then
	display_configuration
	exit 0
elif [[ $flush_creds ]]; then
	flush_credentials
	exit 0
elif [[ $gpg_recipient ]]; then
	export_gpg
	exit 0
elif [[ $gpg_import_file ]]; then
	import_gpg
elif [[ $cipher ]]; then
	validate_cipher
fi

# perform function calls to configure transcrypt
get_cipher
get_password

if [[ $rekey ]] && [[ $interactive ]]; then
	confirm_rekey
elif [[ $interactive ]]; then
	confirm_configuration
fi

save_configuration

if [[ $rekey ]]; then
	stage_rekeyed_files
else
	force_checkout
fi

# ensure the git attributes file exists
if [[ ! -f $GIT_ATTRIBUTES ]]; then
	mkdir -p "${GIT_ATTRIBUTES%/*}"
	printf '#pattern  filter=crypt diff=crypt\n' >"$GIT_ATTRIBUTES"
fi

printf 'The repository has been successfully configured by transcrypt.\n'

exit 0