diff --git a/ixc_django_docker/bin/transcrypt b/ixc_django_docker/bin/transcrypt index 8ca3019..7856391 100755 --- a/ixc_django_docker/bin/transcrypt +++ b/ixc_django_docker/bin/transcrypt @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -euo pipefail # # transcrypt - https://github.com/elasticdog/transcrypt @@ -7,7 +8,7 @@ # 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-2017 Aaron Bull Schaefer +# Copyright (c) 2014-2019 Aaron Bull Schaefer # This source code is provided under the terms of the MIT License # that can be be found in the LICENSE file. # @@ -15,33 +16,12 @@ ##### CONSTANTS # the release version of this script -readonly VERSION='1.0.3' +readonly VERSION='2.0.0' # the default cipher to utilize readonly DEFAULT_CIPHER='aes-256-cbc' -# 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 +##### FUNCTIONS # print a canonicalized absolute pathname realpath() { @@ -59,7 +39,7 @@ realpath() { dirname=$(cd "$abspath" && pwd -P) abspath=$dirname elif [[ -e $abspath ]]; then - dirname=$(cd "${abspath%/*}/" 2> /dev/null && pwd -P) + dirname=$(cd "${abspath%/*}/" 2>/dev/null && pwd -P) abspath=$dirname/${abspath##*/} fi @@ -71,27 +51,45 @@ realpath() { 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) +# establish repository metadata and directory handling +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 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 + # 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) -##### FUNCTIONS + # 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) + + # the current git repository's gitattributes file + local CORE_ATTRIBUTES + CORE_ATTRIBUTES=$(git config --get --local --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 } @@ -113,7 +111,9 @@ run_safety_checks() { [[ $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 + 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' @@ -121,7 +121,7 @@ run_safety_checks() { # 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" + 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 @@ -136,21 +136,25 @@ run_safety_checks() { # unset the cipher variable if it is not supported by openssl validate_cipher() { - # Support for OpenSSL versions after and before 1.1.0 - local list_cipher_commands="openssl list-cipher-commands" - if [[ $($list_cipher_commands 2> /dev/null) ]]; then - local supported=$(openssl list-cipher-commands | grep --line-regexp "$cipher") + local list_cipher_commands + if openssl list-cipher-commands &>/dev/null; then + # OpenSSL < v1.1.0 + list_cipher_commands='openssl list-cipher-commands' else - list_cipher_commands="openssl list -cipher-commands" - local supported=$(openssl list -cipher-commands | tr -s ' ' '\n' | grep --line-regexp "$cipher") + # OpenSSL >= v1.1.0 + list_cipher_commands='openssl list -cipher-commands' fi + + local supported + supported=$($list_cipher_commands | 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 | column -c 80 printf '\n' - unset cipher + cipher='' else + # shellcheck disable=SC2016 die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands" fi fi @@ -158,9 +162,8 @@ validate_cipher() { # ensure we have a cipher to encrypt with get_cipher() { - while [[ ! $cipher ]] - do - local answer + while [[ ! $cipher ]]; do + local answer= if [[ $interactive ]]; then printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER" read -r answer @@ -179,9 +182,8 @@ get_cipher() { # ensure we have a password to encrypt with get_password() { - while [[ ! $password ]] - do - local answer + while [[ ! $password ]]; do + local answer= if [[ $interactive ]]; then printf 'Generate a random password? [Y/n] ' read -r -n 1 -s answer @@ -192,22 +194,23 @@ get_password() { # otherwise prompt the user for a password if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then local password_length=30 - local random_base64=$(openssl rand -base64 $password_length) + 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' + [[ $password ]] || printf 'no password was specified\n' fi done } # confirm the transcrypt configuration confirm_configuration() { - local answer + local answer= printf '\nRepository metadata:\n\n' - [[ $REPO ]] && printf ' GIT_WORK_TREE: %s\n' "$REPO" + [[ ! $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' @@ -227,10 +230,10 @@ confirm_configuration() { # confirm the rekey configuration confirm_rekey() { - local answer + local answer= printf '\nRepository metadata:\n\n' - [[ $REPO ]] && printf ' GIT_WORK_TREE: %s\n' "$REPO" + [[ ! $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' @@ -251,11 +254,14 @@ confirm_rekey() { # automatically stage rekeyed files in preparation for the user to commit them stage_rekeyed_files() { - local encrypted_files=$(git ls-crypt) + 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' @@ -265,7 +271,7 @@ stage_rekeyed_files() { # save helper scripts under the repository's git directory save_helper_scripts() { - [[ ! -d "${GIT_DIR}/crypt" ]] && mkdir "${GIT_DIR}/crypt" + mkdir -p "${GIT_DIR}/crypt" # The `decryption -> encryption` process on an unchanged file must be # deterministic for everything to work transparently. To do that, the same @@ -274,57 +280,150 @@ save_helper_scripts() { # (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" + 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) + tempfile=$(mktemp 2>/dev/null || mktemp -t tmp) trap 'rm -f "$tempfile"' EXIT - tee "$tempfile" &> /dev/null + tee "$tempfile" &>/dev/null # the first bytes of an encrypted file are always "Salted" in Base64 - read -n 8 firstbytes < "$tempfile" + 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" | tail -c 16) + salt=$(openssl dgst -hmac "${filename}:${password}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16) ENC_PASS=$password openssl enc -$cipher -md MD5 -pass env:ENC_PASS -e -a -S "$salt" -in "$tempfile" fi fi EOF - cat <<-'EOF' > "${GIT_DIR}/crypt/smudge" + cat <<-'EOF' >"${GIT_DIR}/crypt/smudge" #!/usr/bin/env bash - tempfile=$(mktemp 2> /dev/null || mktemp -t tmp) + 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 enc -$cipher -md MD5 -pass env:ENC_PASS -d -a 2> /dev/null || cat "$tempfile" + tee "$tempfile" | ENC_PASS=$password openssl enc -$cipher -md MD5 -pass env:ENC_PASS -d -a 2>/dev/null || cat "$tempfile" EOF - cat <<-'EOF' > "${GIT_DIR}/crypt/textconv" + 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 enc -$cipher -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2> /dev/null || cat "$filename" + ENC_PASS=$password openssl enc -$cipher -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2>/dev/null || cat "$filename" + fi + EOF + + cat <<-'EOF' >"${GIT_DIR}/crypt/merge" + #!/usr/bin/env bash + + # 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" | ./.git/crypt/smudge)" > "$1" + echo "$(cat "$2" | ./.git/crypt/smudge)" > "$2" + echo "$(cat "$3" | ./.git/crypt/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" | ./.git/crypt/clean "$5" > "$2" EOF # make scripts executable - for script in {clean,smudge,textconv}; do + for script in {clean,smudge,textconv,merge}; do chmod 0755 "${GIT_DIR}/crypt/${script}" done } +# save helper hooks under the repository's git directory +save_helper_hooks() { + # Install pre-commit-crypt hook script + pre_commit_hook_installed="${RELATIVE_GIT_DIR}/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 + IFS=$'\n' + 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 + # Get prefix of raw file in Git's index using the :FILENAME revision syntax + firstbytes=$(git show :$secret_file | head -c 8) + # 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 reset -- %s\n' $secret_file >&2 + printf ' git add %s\n' $secret_file >&2 + printf '\n' >&2 + exit 1 + fi + done + unset IFS + EOF + + # Activate hook by copying it to the pre-commit script name, if safe to do so + pre_commit_hook="${RELATIVE_GIT_DIR}/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" @@ -335,32 +434,45 @@ save_configuration() { 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' + # shellcheck disable=SC2016 + git config merge.crypt.driver '"$(git rev-parse --git-common-dir)"/crypt/merge %O %A %B %L %P' 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' + # shellcheck disable=SC2016 + git config merge.crypt.driver '"$(git rev-parse --git-dir)"/crypt/merge %O %A %B %L %P' fi 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 ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'" + 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=$(git config --get --local transcrypt.cipher) - local current_password=$(git config --get --local transcrypt.password) + 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" + [[ ! $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" @@ -371,15 +483,17 @@ display_configuration() { # remove transcrypt-related settings from the repository's git config clean_gitconfig() { - git config --remove-section transcrypt - git config --remove-section filter.crypt - git config --remove-section diff.crypt + 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=$(git config --get-regex --local 'merge\..*') + local merge_values + merge_values=$(git config --get-regex --local 'merge\..*') || true if [[ ! $merge_values ]]; then - git config --remove-section merge + git config --remove-section merge 2>/dev/null || true fi } @@ -391,12 +505,13 @@ force_checkout() { 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=$(git ls-crypt) + 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 + git checkout --force HEAD -- "$file" >/dev/null done unset IFS fi @@ -405,7 +520,7 @@ force_checkout() { # remove the locally cached encryption credentials and # re-encrypt any files that had been previously decrypted flush_credentials() { - local answer + local answer= if [[ $interactive ]]; then printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n' @@ -433,7 +548,7 @@ flush_credentials() { # remove all transcrypt configuration from the repository uninstall_transcrypt() { - local answer + local answer= if [[ $interactive ]]; then printf 'You are about to remove all transcrypt configuration from your repository.\n' @@ -451,15 +566,31 @@ uninstall_transcrypt() { clean_gitconfig # remove helper scripts - for script in {clean,smudge,textconv}; do - [[ -f "${GIT_DIR}/crypt/${script}" ]] && rm "${GIT_DIR}/crypt/${script}" + for script in {clean,smudge,textconv,merge}; do + [[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}" done - [[ -d "${GIT_DIR}/crypt" ]] && rmdir "${GIT_DIR}/crypt" + [[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt" + + # rename helper hooks (don't delete, in case user has custom changes) + pre_commit_hook="${RELATIVE_GIT_DIR}/hooks/pre-commit" + pre_commit_hook_installed="${RELATIVE_GIT_DIR}/hooks/pre-commit-crypt" + if [ -f "$pre_commit_hook" ]; then + hook_md5=$(openssl md5 -hex <"$pre_commit_hook") + installed_md5=$(openssl 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=$(git ls-crypt) + 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 @@ -467,19 +598,22 @@ uninstall_transcrypt() { git config --unset alias.ls-crypt # remove the alias section if it's now empty - local alias_values=$(git config --get-regex --local 'alias\..*') + local alias_values + alias_values=$(git config --get-regex --local 'alias\..*') || true if [[ ! $alias_values ]]; then - git config --remove-section alias + git config --remove-section alias 2>/dev/null || true fi # remove any defined crypt patterns in gitattributes case $OSTYPE in - darwin*) - sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES" - ;; - linux*) - sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES" - ;; + 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 printf 'The transcrypt configuration has been completely removed from the repository.\n' @@ -492,7 +626,7 @@ uninstall_transcrypt() { 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 }' + git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }' fi } @@ -501,8 +635,8 @@ 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") + 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 @@ -524,26 +658,28 @@ show_raw_file() { # 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' + 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 + 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=$(git config --get --local transcrypt.cipher) - local current_password=$(git config --get --local transcrypt.password) - [[ ! -d "${GIT_DIR}/crypt" ]] && mkdir "${GIT_DIR}/crypt" + 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 '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' + command -v gpg >/dev/null || die 'required command "gpg" was not found' local path if [[ -f "${GIT_DIR}/crypt/${gpg_import_file}" ]]; then @@ -556,10 +692,9 @@ import_gpg() { path="$gpg_import_file" fi - local configuration - local safety_counter # fix for intermittent 'no secret key' decryption failures - while [[ ! $configuration ]] - do + 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)) @@ -575,244 +710,251 @@ import_gpg() { # print this script's usage message to stderr usage() { cat <<-EOF >&2 - usage: transcrypt [-c CIPHER] [-p PASSWORD] [-h] + 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 - NAME - transcrypt -- transparently encrypt files within a git repository - - SYNOPSIS - transcrypt [options...] - - DESCRIPTION + SYNOPSIS + transcrypt [options...] - 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. + DESCRIPTION - The transcrypt source code and full documentation may be downloaded - from https://github.com/elasticdog/transcrypt. + 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. - OPTIONS - -c, --cipher=CIPHER - the symmetric cipher to utilize for encryption; - defaults to aes-256-cbc + The transcrypt source code and full documentation may be downloaded + from https://github.com/elasticdog/transcrypt. - -p, --password=PASSWORD - the password to derive the key from; - defaults to 30 random base64 characters + OPTIONS + -c, --cipher=CIPHER + the symmetric cipher to utilize for encryption; + defaults to aes-256-cbc - -y, --yes - assume yes and accept defaults for non-specified options + -p, --password=PASSWORD + the password to derive the key from; + defaults to 30 random base64 characters - -d, --display - display the current repository's cipher and password + -y, --yes + assume yes and accept defaults for non-specified options - -r, --rekey - re-encrypt all encrypted files using new credentials + -d, --display + display the current repository's cipher and password - -f, --flush-credentials - remove the locally cached encryption credentials and re-encrypt - any files that had been previously decrypted + -r, --rekey + re-encrypt all encrypted files using new credentials - -F, --force - ignore whether the git directory is clean, proceed with the - possibility that uncommitted changes are overwritten + -f, --flush-credentials + remove the locally cached encryption credentials and re-encrypt + any files that had been previously decrypted - -u, --uninstall - remove all transcrypt configuration from the repository and - leave files in the current working copy decrypted + -F, --force + ignore whether the git directory is clean, proceed with the + possibility that uncommitted changes are overwritten - -l, --list - list all of the transparently encrypted files in the repository, - relative to the top-level directory + -u, --uninstall + remove all transcrypt configuration from the repository and + leave files in the current working copy decrypted - -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 + -l, --list + list all of the transparently encrypted files in the repository, + relative to the top-level directory - -e, --export-gpg=RECIPIENT - export the repository's cipher and password to a file encrypted - for a gpg recipient + -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 - -i, --import-gpg=FILE - import the password and cipher from a gpg encrypted file + -e, --export-gpg=RECIPIENT + export the repository's cipher and password to a file encrypted + for a gpg recipient - -v, --version - print the version information + -i, --import-gpg=FILE + import the password and cipher from a gpg encrypted file - -h, --help - view this help message + -v, --version + print the version information - EXAMPLES + -h, --help + view this help message - 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. + EXAMPLES - $ cd / - $ transcrypt + 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. - 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: + $ cd / + $ transcrypt - $ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes - $ git add .gitattributes sensitive_file - $ git commit -m 'Add encrypted version of a sensitive file' + 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: - See the gitattributes(5) man page for more information. + $ 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' - 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. + See the gitattributes(5) man page for more information. - 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. + 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. - AUTHOR - Aaron Bull Schaefer + 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. - SEE ALSO - enc(1), gitattributes(5) + AUTHOR + Aaron Bull Schaefer + 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='' +gpg_recipient='' +interactive='true' +list='' +password='' +rekey='' +show_file='' +uninstall='' # 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 +while [[ "${1:-}" != '' ]]; do case $1 in - -c | --cipher) - cipher=$2 - shift - ;; - --cipher=*) - cipher=${1#*=} - ;; - -p | --password) - password=$2 - shift - ;; - --password=*) - password=${1#*=} - ;; - -y | --yes) - unset interactive - ;; - -d | --display) - display_config='true' - requires_existing_config='true' - unset requires_clean_repo - ;; - -r | --rekey) - rekey='true' - requires_existing_config='true' - ;; - -f | --flush-credentials) - flush_creds='true' - requires_existing_config='true' - ;; - -F | --force) - unset requires_clean_repo - ;; - -u | --uninstall) - uninstall='true' - requires_existing_config='true' - unset 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' - unset requires_clean_repo - shift - ;; - --export-gpg=*) - gpg_recipient=${1#*=} - requires_existing_config='true' - unset 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 - ;; + -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='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 [[ $uninstall ]]; then +if [[ $list ]]; then + list_files + exit 0 +elif [[ $uninstall ]]; then uninstall_transcrypt exit 0 elif [[ $display_config ]] && [[ $flush_creds ]]; then @@ -856,7 +998,7 @@ 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" + printf '#pattern filter=crypt diff=crypt merge=crypt\n' >"$GIT_ATTRIBUTES" fi printf 'The repository has been successfully configured by transcrypt.\n' diff --git a/ixc_django_docker/bin/transcrypt-v1-compatible b/ixc_django_docker/bin/transcrypt-v1-compatible new file mode 100755 index 0000000..ed89f77 --- /dev/null +++ b/ixc_django_docker/bin/transcrypt-v1-compatible @@ -0,0 +1,1011 @@ +#!/usr/bin/env bash +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 +# 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-cbc' + +##### 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 +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) + + # the current git repository's gitattributes file + local CORE_ATTRIBUTES + CORE_ATTRIBUTES=$(git config --get --local --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" +} + +# 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,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 + if openssl list-cipher-commands &>/dev/null; then + # OpenSSL < v1.1.0 + list_cipher_commands='openssl list-cipher-commands' + else + # OpenSSL >= v1.1.0 + list_cipher_commands='openssl list -cipher-commands' + fi + + local supported + supported=$($list_cipher_commands | 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 | 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 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" + + # 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) + + # Undo salt generation fix from c8e4f727 to keep compatibility with + # Transcrypt versions prior to 2.0 + # salt=$(openssl dgst -hmac "${filename}:${password}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16) + salt=$(openssl dgst -hmac "${filename}:${password}" -sha256 "$filename" | tail -c 16) + + ENC_PASS=$password openssl enc -$cipher -md MD5 -pass env:ENC_PASS -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 enc -$cipher -md MD5 -pass env:ENC_PASS -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 enc -$cipher -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2>/dev/null || cat "$filename" + fi + EOF + + cat <<-'EOF' >"${GIT_DIR}/crypt/merge" + #!/usr/bin/env bash + + # 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" | ./.git/crypt/smudge)" > "$1" + echo "$(cat "$2" | ./.git/crypt/smudge)" > "$2" + echo "$(cat "$3" | ./.git/crypt/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" | ./.git/crypt/clean "$5" > "$2" + EOF + + # make scripts executable + for script in {clean,smudge,textconv,merge}; do + chmod 0755 "${GIT_DIR}/crypt/${script}" + done +} + +# save helper hooks under the repository's git directory +save_helper_hooks() { + # Install pre-commit-crypt hook script + pre_commit_hook_installed="${RELATIVE_GIT_DIR}/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 + IFS=$'\n' + 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 + # Get prefix of raw file in Git's index using the :FILENAME revision syntax + firstbytes=$(git show :$secret_file | head -c 8) + # 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 reset -- %s\n' $secret_file >&2 + printf ' git add %s\n' $secret_file >&2 + printf '\n' >&2 + exit 1 + fi + done + unset IFS + EOF + + # Activate hook by copying it to the pre-commit script name, if safe to do so + pre_commit_hook="${RELATIVE_GIT_DIR}/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" + + # 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' + # shellcheck disable=SC2016 + git config merge.crypt.driver '"$(git rev-parse --git-common-dir)"/crypt/merge %O %A %B %L %P' + 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' + # shellcheck disable=SC2016 + git config merge.crypt.driver '"$(git rev-parse --git-dir)"/crypt/merge %O %A %B %L %P' + fi + 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 +} + +# 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,merge}; do + [[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}" + done + [[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt" + + # rename helper hooks (don't delete, in case user has custom changes) + pre_commit_hook="${RELATIVE_GIT_DIR}/hooks/pre-commit" + pre_commit_hook_installed="${RELATIVE_GIT_DIR}/hooks/pre-commit-crypt" + if [ -f "$pre_commit_hook" ]; then + hook_md5=$(openssl md5 -hex <"$pre_commit_hook") + installed_md5=$(openssl 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 + + 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 -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 "${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 / + $ 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 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 + + 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='' + +# 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 + -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='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 [[ $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