1202 lines
38 KiB
Plaintext
Executable File
1202 lines
38 KiB
Plaintext
Executable File
#!/usr/bin/env nix-shell
|
|
#! nix-shell -i bash --pure
|
|
#! nix-shell -p bash openssl git unixtools.column perl
|
|
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.2.0'
|
|
|
|
# the default cipher to utilize
|
|
readonly DEFAULT_CIPHER='aes-256-ctr'
|
|
|
|
# arguments of the openssl enc command
|
|
readonly ENCRYPT_OPTIONS='-pbkdf2 -iter 200000 -pass env:ENC_PASS'
|
|
|
|
##### FUNCTIONS
|
|
|
|
# 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
|
|
}
|
|
|
|
# establish repository metadata and directory handling
|
|
# shellcheck disable=SC2155
|
|
gather_repo_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 || printf 'false')
|
|
|
|
# the current git repository's .git directory
|
|
readonly RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '')
|
|
readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null)
|
|
|
|
# Respect transcrypt.crypt-dir if present. Default to crypt/ in Git dir
|
|
readonly CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}")
|
|
|
|
# respect core.hooksPath setting, without trailing slash. Fall back to default hooks dir
|
|
readonly GIT_HOOKS=$(git config core.hooksPath | sed 's:/*$::' 2>/dev/null || printf "%s/hooks" "${RELATIVE_GIT_DIR}")
|
|
|
|
# the current git repository's gitattributes file
|
|
local CORE_ATTRIBUTES
|
|
CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile 2>/dev/null || git config --get --path core.attributesFile 2>/dev/null || printf '')
|
|
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
|
|
}
|
|
|
|
# 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"
|
|
}
|
|
|
|
# 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.
|
|
|
|
git_clean() {
|
|
filename=$1
|
|
# ignore empty files
|
|
if [[ ! -s $filename ]]; then
|
|
return
|
|
fi
|
|
# 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
|
|
# The `head + LC_ALL=C tr` command handles binary data in old and new Bash (#116)
|
|
firstbytes=$(head -c8 "$tempfile" | LC_ALL=C tr -d '\0')
|
|
if [[ $firstbytes == "U2FsdGVk" ]]; then
|
|
cat "$tempfile"
|
|
else
|
|
cipher=$(git config --get --local transcrypt.cipher)
|
|
password=$(git config --get --local transcrypt.password)
|
|
openssl_path=$(git config --get --local transcrypt.openssl-path)
|
|
salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16)
|
|
|
|
openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1)
|
|
if [ "$openssl_major_version" -ge "3" ]; then
|
|
# Encrypt the file to base64, ensuring it includes the prefix 'Salted__' with the salt. #133
|
|
(
|
|
echo -n "Salted__" && echo -n "$salt" | perl -pe 's/(..)/chr(hex($1))/ge' &&
|
|
# Encrypt file to binary ciphertext
|
|
ENC_PASS="$password" "$openssl_path" enc -e -$cipher $ENCRYPT_OPTIONS -S "$salt" -in "$tempfile"
|
|
) |
|
|
openssl base64
|
|
else
|
|
# Encrypt file to base64 ciphertext
|
|
ENC_PASS="$password" "$openssl_path" enc -e -a -$cipher $ENCRYPT_OPTIONS -S "$salt" -in "$tempfile"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
git_smudge() {
|
|
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)
|
|
openssl_path=$(git config --get --local transcrypt.openssl-path)
|
|
tee "$tempfile" | ENC_PASS="$password" "$openssl_path" enc -d -$cipher $ENCRYPT_OPTIONS -a 2>/dev/null || cat "$tempfile"
|
|
}
|
|
|
|
git_textconv() {
|
|
filename=$1
|
|
# ignore empty files
|
|
if [[ ! -s $filename ]]; then
|
|
return
|
|
fi
|
|
cipher=$(git config --get --local transcrypt.cipher)
|
|
password=$(git config --get --local transcrypt.password)
|
|
openssl_path=$(git config --get --local transcrypt.openssl-path)
|
|
ENC_PASS="$password" "$openssl_path" enc -d -$cipher $ENCRYPT_OPTIONS -a -in "$filename" 2>/dev/null || cat "$filename"
|
|
}
|
|
|
|
# shellcheck disable=SC2005,SC2002,SC2181
|
|
git_merge() {
|
|
# Get path to transcrypt in this script's directory
|
|
TRANSCRYPT_PATH="$(dirname "$0")/transcrypt"
|
|
# Look up name of local branch/ref to which changes are being merged
|
|
OURS_LABEL=$(git rev-parse --abbrev-ref HEAD)
|
|
# Look up name of the incoming "theirs" branch/ref being merged in.
|
|
# TODO There must be a better way of doing this than relying on this reflog
|
|
# action environment variable, but I don't know what it is
|
|
if [[ "$GIT_REFLOG_ACTION" = "merge "* ]]; then
|
|
THEIRS_LABEL=$(echo "$GIT_REFLOG_ACTION" | awk '{print $2}')
|
|
fi
|
|
if [[ ! "$THEIRS_LABEL" ]]; then
|
|
THEIRS_LABEL="theirs"
|
|
fi
|
|
# Decrypt BASE $1, LOCAL $2, and REMOTE $3 versions of file being merged
|
|
echo "$(cat "$1" | "${TRANSCRYPT_PATH}" smudge)" >"$1"
|
|
echo "$(cat "$2" | "${TRANSCRYPT_PATH}" smudge)" >"$2"
|
|
echo "$(cat "$3" | "${TRANSCRYPT_PATH}" smudge)" >"$3"
|
|
# Merge the decrypted files to the temp file named by $2
|
|
git merge-file --marker-size="$4" -L "$OURS_LABEL" -L base -L "$THEIRS_LABEL" "$2" "$1" "$3"
|
|
# If the merge was not successful (has conflicts) exit with an error code to
|
|
# leave the partially-merged file in place for a manual merge.
|
|
if [[ "$?" != "0" ]]; then
|
|
exit 1
|
|
fi
|
|
# If the merge was successful (no conflicts) re-encrypt the merged temp file $2
|
|
# which git will then update in the index in a following "Auto-merging" step.
|
|
# We must explicitly encrypt/clean the file, rather than leave Git to do it,
|
|
# because we can otherwise trigger safety check failure errors like:
|
|
# error: add_cacheinfo failed to refresh for path 'FILE'; merge aborting.
|
|
# To re-encrypt we must first copy the merged file to $5 (the name of the
|
|
# working-copy file) so the crypt `clean` script can generate the correct hash
|
|
# salt based on the file's real name, instead of the $2 temp file name.
|
|
cp "$2" "$5"
|
|
# Now we use the `clean` script to encrypt the merged file contents back to the
|
|
# temp file $2 where Git expects to find the merge result content.
|
|
cat "$5" | "${TRANSCRYPT_PATH}" clean "$5" >"$2"
|
|
}
|
|
|
|
# shellcheck disable=SC2155
|
|
git_pre_commit() {
|
|
# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64
|
|
tmp=$(mktemp)
|
|
IFS=$'\n'
|
|
slow_mode_if_failed() {
|
|
for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do
|
|
# Skip symlinks, they contain the linked target file path not plaintext
|
|
if [[ -L $secret_file ]]; then
|
|
continue
|
|
fi
|
|
|
|
# Get prefix of raw file in Git's index using the :FILENAME revision syntax
|
|
local firstbytes=$(git show :"${secret_file}" | head -c8)
|
|
# An empty file does not need to be, and is not, encrypted
|
|
if [[ $firstbytes == "" ]]; then
|
|
: # Do nothing
|
|
# The first bytes of an encrypted file must be "Salted" in Base64
|
|
elif [[ $firstbytes != "U2FsdGVk" ]]; then
|
|
printf 'Transcrypt managed file is not encrypted in the Git index: %s\n' "$secret_file" >&2
|
|
printf '\n' >&2
|
|
printf 'You probably staged this file using a tool that does not apply' >&2
|
|
printf ' .gitattribute filters as required by Transcrypt.\n' >&2
|
|
printf '\n' >&2
|
|
printf 'Fix this by re-staging the file with a compatible tool or with'
|
|
printf ' Git on the command line:\n' >&2
|
|
printf '\n' >&2
|
|
printf ' git rm --cached -- %s\n' "$secret_file" >&2
|
|
printf ' git add %s\n' "$secret_file" >&2
|
|
printf '\n' >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
# validate file to see if it failed or not, We don't care about the filename currently for speed, we only care about pass/fail, slow_mode_if_failed() is for what failed.
|
|
validate_file() {
|
|
secret_file=${1}
|
|
# Skip symlinks, they contain the linked target file path not plaintext
|
|
if [[ -L $secret_file ]]; then
|
|
return
|
|
fi
|
|
# Get prefix of raw file in Git's index using the :FILENAME revision syntax
|
|
# The first bytes of an encrypted file are always "Salted" in Base64
|
|
local firstbytes=$(git show :"${secret_file}" | head -c8)
|
|
if [[ $firstbytes != "U2FsdGVk" ]]; then
|
|
echo "true" >>"${tmp}"
|
|
fi
|
|
}
|
|
|
|
# if bash version is 4.4 or greater than fork to number of threads otherwise run normally
|
|
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]] && [[ "${BASH_VERSINFO[1]}" -ge 4 ]]; then
|
|
num_procs=$(nproc)
|
|
num_jobs="\j"
|
|
for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do
|
|
while ((${num_jobs@P} >= num_procs)); do
|
|
wait -n
|
|
done
|
|
validate_file "${secret_file}" &
|
|
done
|
|
wait
|
|
if [[ -s ${tmp} ]]; then
|
|
slow_mode_if_failed
|
|
rm -f "${tmp}"
|
|
exit 1
|
|
fi
|
|
else
|
|
slow_mode_if_failed
|
|
fi
|
|
|
|
rm -f "${tmp}"
|
|
unset IFS
|
|
}
|
|
|
|
# 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 [[ $ignore_config_status ]]; then
|
|
: # no-op, no need to check $CONFIGURED status
|
|
elif [[ $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_path}",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
|
|
# ensure index is up-to-date before dirty check
|
|
git update-index -q --really-refresh
|
|
# 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_path} enc -ciphers"
|
|
|
|
local supported
|
|
supported=$($list_cipher_commands | tr -s ' ' '\n' | grep -Fx -- "-$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 | column -c 80
|
|
printf '\n'
|
|
cipher=''
|
|
else
|
|
# shellcheck disable=SC2016
|
|
die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands"
|
|
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_path} 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 "${CRYPT_DIR}"
|
|
|
|
local current_transcrypt
|
|
current_transcrypt=$(realpath "$0" 2>/dev/null)
|
|
echo '#!/usr/bin/env bash' > "${CRYPT_DIR}/transcrypt"
|
|
tail -n +4 "$current_transcrypt" >> "${CRYPT_DIR}/transcrypt"
|
|
|
|
# make scripts executable
|
|
for script in {transcrypt,}; do
|
|
chmod 0755 "${CRYPT_DIR}/${script}"
|
|
done
|
|
}
|
|
|
|
# save helper hooks under the repository's git directory
|
|
save_helper_hooks() {
|
|
# Install pre-commit-crypt hook script
|
|
[[ ! -d "${GIT_HOOKS}" ]] && mkdir -p "${GIT_HOOKS}"
|
|
pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt"
|
|
cat <<-'EOF' >"$pre_commit_hook_installed"
|
|
#!/usr/bin/env bash
|
|
# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64
|
|
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '')
|
|
CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}")
|
|
"${CRYPT_DIR}/transcrypt" pre_commit
|
|
EOF
|
|
|
|
# Activate hook by copying it to the pre-commit script name, if safe to do so
|
|
pre_commit_hook="${GIT_HOOKS}/pre-commit"
|
|
if [[ -f "$pre_commit_hook" ]]; then
|
|
printf 'WARNING:\n' >&2
|
|
printf 'Cannot install Git pre-commit hook script because file already exists: %s\n' "$pre_commit_hook" >&2
|
|
printf 'Please manually install the pre-commit script saved as: %s\n' "$pre_commit_hook_installed" >&2
|
|
printf '\n'
|
|
else
|
|
cp "$pre_commit_hook_installed" "$pre_commit_hook"
|
|
chmod 0755 "$pre_commit_hook"
|
|
fi
|
|
}
|
|
|
|
# write the configuration to the repository's git config
|
|
save_configuration() {
|
|
save_helper_scripts
|
|
save_helper_hooks
|
|
|
|
# write the encryption info
|
|
git config transcrypt.version "$VERSION"
|
|
git config transcrypt.cipher "$cipher"
|
|
git config transcrypt.password "$password"
|
|
git config transcrypt.openssl-path "$openssl_path"
|
|
|
|
# write the filter settings. Sorry for the horrific quote escaping below...
|
|
# shellcheck disable=SC2016
|
|
git config filter.crypt.clean '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt clean %f'
|
|
# shellcheck disable=SC2016
|
|
git config filter.crypt.smudge '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt smudge'
|
|
# shellcheck disable=SC2016
|
|
git config diff.crypt.textconv '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt textconv'
|
|
# shellcheck disable=SC2016
|
|
git config merge.crypt.driver '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt merge %O %A %B %L %P'
|
|
git config filter.crypt.required 'true'
|
|
git config diff.crypt.cachetextconv 'true'
|
|
git config diff.crypt.binary 'true'
|
|
git config merge.renormalize 'true'
|
|
git config merge.crypt.name 'Merge transcrypt secret files'
|
|
|
|
# add a git alias for listing encrypted files
|
|
git config alias.ls-crypt "!git -c core.quotePath=false ls-files | git -c core.quotePath=false 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 --remove-section merge.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
|
|
}
|
|
|
|
# Remove from the local Git DB any objects containing the cached plaintext of
|
|
# secret files, created due to the setting diff.crypt.cachetextconv='true'
|
|
remove_cached_plaintext() {
|
|
# Delete ref to cached plaintext objects, to leave these objects
|
|
# unreferenced and available for removal
|
|
git update-ref -d refs/notes/textconv/crypt
|
|
|
|
# Remove ANY unreferenced objects in Git's object DB (packed or unpacked),
|
|
# to ensure that cached plaintext objects are also removed.
|
|
# The vital sub-commands equivalents we require this `gc` command to do are:
|
|
# `git prune`, `git repack -ad`
|
|
git gc --prune=now --quiet
|
|
}
|
|
|
|
# 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 -f "$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, and your\n'
|
|
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\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
|
|
|
|
remove_cached_plaintext
|
|
|
|
# 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, but your\n'
|
|
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\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
|
|
|
|
if [[ ! $upgrade ]]; then
|
|
remove_cached_plaintext
|
|
fi
|
|
|
|
# remove helper scripts
|
|
# Keep obsolete clean,smudge,textconv,merge refs here to remove them on upgrade
|
|
for script in {transcrypt,clean,smudge,textconv,merge}; do
|
|
[[ ! -f "${CRYPT_DIR}/${script}" ]] || rm "${CRYPT_DIR}/${script}"
|
|
done
|
|
[[ ! -d "${CRYPT_DIR}" ]] || rmdir "${CRYPT_DIR}"
|
|
|
|
# rename helper hooks (don't delete, in case user has custom changes)
|
|
pre_commit_hook="${GIT_HOOKS}/pre-commit"
|
|
pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt"
|
|
if [[ -f "$pre_commit_hook" ]]; then
|
|
hook_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook")
|
|
installed_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook_installed")
|
|
if [[ "$hook_md5" = "$installed_md5" ]]; then
|
|
rm "$pre_commit_hook"
|
|
else
|
|
printf 'WARNING: Cannot safely disable Git pre-commit hook %s please check it yourself\n' "$pre_commit_hook"
|
|
fi
|
|
fi
|
|
[[ -f "$pre_commit_hook_installed" ]] && rm "$pre_commit_hook_installed"
|
|
|
|
# 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"
|
|
/usr/bin/sed -i '' '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
|
|
;;
|
|
linux*)
|
|
sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
|
|
sed -i '/filter=crypt diff=crypt merge=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
|
|
;;
|
|
esac
|
|
|
|
if [[ ! $upgrade ]]; then
|
|
printf 'The transcrypt configuration has been completely removed from the repository.\n'
|
|
fi
|
|
else
|
|
die 1 'uninstallation has been aborted'
|
|
fi
|
|
}
|
|
|
|
# uninstall and re-install transcrypt to upgrade scripts and update configuration
|
|
upgrade_transcrypt() {
|
|
CURRENT_VERSION=$(git config --get --local transcrypt.version 2>/dev/null)
|
|
|
|
if [[ $interactive ]]; then
|
|
printf 'You are about to upgrade the transcrypt scripts in your repository.\n'
|
|
printf 'Your configuration settings will not be changed.\n\n'
|
|
printf ' Current version: %s\n' "$CURRENT_VERSION"
|
|
printf 'Upgraded version: %s\n\n' "$VERSION"
|
|
printf 'Proceed with upgrade? [y/N] '
|
|
read -r answer
|
|
printf '\n'
|
|
|
|
if [[ $answer =~ $YES_REGEX ]]; then
|
|
# User confirmed, don't prompt again
|
|
interactive=''
|
|
else
|
|
# User did not confirm, exit
|
|
# Exit if user did not confirm
|
|
die 1 'upgrade has been aborted'
|
|
fi
|
|
fi
|
|
|
|
# Keep current cipher and password
|
|
cipher=$(git config --get --local transcrypt.cipher)
|
|
password=$(git config --get --local transcrypt.password)
|
|
# Keep current openssl-path, or set to default if no existing value
|
|
openssl_path=$(git config --get --local transcrypt.openssl-path 2>/dev/null || printf '%s' "$openssl_path")
|
|
|
|
# Keep contents of .gitattributes
|
|
ORIG_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES")
|
|
|
|
uninstall_transcrypt
|
|
save_configuration
|
|
|
|
# Re-instate contents of .gitattributes
|
|
echo "$ORIG_GITATTRIBUTES" >"$GIT_ATTRIBUTES"
|
|
|
|
# Update .gitattributes for transcrypt'ed files to include "merge=crypt" config
|
|
case $OSTYPE in
|
|
darwin*)
|
|
/usr/bin/sed -i '' 's/=crypt\(.*\)/=crypt diff=crypt merge=crypt/' "$GIT_ATTRIBUTES"
|
|
;;
|
|
linux*)
|
|
sed -i 's/=crypt\(.*\)/=crypt diff=crypt merge=crypt/' "$GIT_ATTRIBUTES"
|
|
;;
|
|
esac
|
|
|
|
printf 'Upgrade is complete\n'
|
|
|
|
LATEST_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES")
|
|
if [[ "$LATEST_GITATTRIBUTES" != "$ORIG_GITATTRIBUTES" ]]; then
|
|
printf '\nYour gitattributes file has been updated with the latest recommended values.\n'
|
|
printf 'Please review and commit the new values in:\n'
|
|
printf '%s\n' "$GIT_ATTRIBUTES"
|
|
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 -c core.quotePath=false ls-files | git -c core.quotePath=false 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 -c core.quotePath=false ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then
|
|
file_paths=$(git -c core.quotePath=false 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 "${CRYPT_DIR}"
|
|
|
|
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 >"${CRYPT_DIR}/${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 "${CRYPT_DIR}/${gpg_import_file}" ]]; then
|
|
path="${CRYPT_DIR}/${gpg_import_file}"
|
|
elif [[ -f "${CRYPT_DIR}/${gpg_import_file}.asc" ]]; then
|
|
path="${CRYPT_DIR}/${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
|
|
|
|
--set-openssl-path=PATH_TO_OPENSSL
|
|
use OpenSSL at this path; defaults to 'openssl' in \$PATH
|
|
|
|
-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
|
|
|
|
--upgrade
|
|
apply the latest transcrypt scripts in the repository without
|
|
changing your configuration settings
|
|
|
|
-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, diff and merge
|
|
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 merge=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=''
|
|
display_config=''
|
|
flush_creds=''
|
|
gpg_import_file=''
|
|
gpg_recipient=''
|
|
interactive='true'
|
|
list=''
|
|
password=''
|
|
rekey=''
|
|
show_file=''
|
|
uninstall=''
|
|
upgrade=''
|
|
openssl_path='openssl'
|
|
|
|
# used to bypass certain safety checks
|
|
requires_existing_config=''
|
|
requires_clean_repo='true'
|
|
ignore_config_status='' # Set for operations where config can exist or not
|
|
|
|
# parse command line options
|
|
while [[ "${1:-}" != '' ]]; do
|
|
case $1 in
|
|
clean)
|
|
shift
|
|
git_clean "$@"
|
|
exit $?
|
|
;;
|
|
smudge)
|
|
shift
|
|
git_smudge "$@"
|
|
exit $?
|
|
;;
|
|
textconv)
|
|
shift
|
|
git_textconv "$@"
|
|
exit $?
|
|
;;
|
|
merge)
|
|
shift
|
|
git_merge "$@"
|
|
exit $?
|
|
;;
|
|
pre_commit)
|
|
shift
|
|
git_pre_commit "$@"
|
|
exit $?
|
|
;;
|
|
-c | --cipher)
|
|
cipher=$2
|
|
shift
|
|
;;
|
|
--cipher=*)
|
|
cipher=${1#*=}
|
|
;;
|
|
-p | --password)
|
|
password=$2
|
|
shift
|
|
;;
|
|
--password=*)
|
|
password=${1#*=}
|
|
;;
|
|
--set-openssl-path=*)
|
|
openssl_path=${1#*=}
|
|
# Immediately apply config setting
|
|
git config transcrypt.openssl-path "$openssl_path"
|
|
;;
|
|
-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=''
|
|
;;
|
|
--upgrade)
|
|
upgrade='true'
|
|
requires_existing_config='true'
|
|
requires_clean_repo=''
|
|
;;
|
|
-l | --list)
|
|
list='true'
|
|
requires_clean_repo=''
|
|
ignore_config_status='true'
|
|
;;
|
|
-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
|
|
|
|
gather_repo_metadata
|
|
|
|
# always run our safety checks
|
|
run_safety_checks
|
|
|
|
# regular expression used to test user input
|
|
readonly YES_REGEX='^[Yy]$'
|
|
|
|
# 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 [[ $list ]]; then
|
|
list_files
|
|
exit 0
|
|
elif [[ $uninstall ]]; then
|
|
uninstall_transcrypt
|
|
exit 0
|
|
elif [[ $upgrade ]]; then
|
|
upgrade_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 merge=crypt\n' >"$GIT_ATTRIBUTES"
|
|
fi
|
|
|
|
printf 'The repository has been successfully configured by transcrypt.\n'
|
|
|
|
exit 0
|