889 lines
26 KiB
Plaintext
Executable File
889 lines
26 KiB
Plaintext
Executable File
#!/usr/bin/env nix-shell
|
|
#! nix-shell -i bash --pure
|
|
#! nix-shell -p bash openssl git unixtools.column
|
|
set -euo pipefail
|
|
|
|
#
|
|
# transcrypt - https://github.com/elasticdog/transcrypt
|
|
#
|
|
# A script to configure transparent encryption of sensitive files stored in
|
|
# a Git repository. It utilizes OpenSSL's symmetric cipher routines and follows
|
|
# the gitattributes(5) man page regarding the use of filters.
|
|
#
|
|
# Copyright (c) 2014-2019 Aaron Bull Schaefer <aaron@elasticdog.com>
|
|
# This source code is provided under the terms of the MIT License
|
|
# that can be be found in the LICENSE file.
|
|
#
|
|
|
|
##### CONSTANTS
|
|
|
|
# the release version of this script
|
|
readonly VERSION='2.0.0'
|
|
|
|
# the default cipher to utilize
|
|
readonly DEFAULT_CIPHER='aes-256-ctr'
|
|
|
|
# the openssl options to encrypt/decrypt the files
|
|
# shellcheck disable=SC2016
|
|
readonly ENCRYPT_OPTIONS='-$cipher -pbkdf2 -iter 200000'
|
|
|
|
# regular expression used to test user input
|
|
readonly YES_REGEX='^[Yy]$'
|
|
|
|
## Repository Metadata
|
|
|
|
# whether or not transcrypt is already configured
|
|
readonly CONFIGURED=$(git config --get --local transcrypt.version 2>/dev/null)
|
|
|
|
# the current git repository's top-level directory
|
|
readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
|
|
# whether or not a HEAD revision exists
|
|
readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null)
|
|
|
|
# https://github.com/RichiH/vcsh
|
|
# whether or not the git repository is running under vcsh
|
|
readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null)
|
|
|
|
# whether or not the git repository is bare
|
|
readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null)
|
|
|
|
## Git Directory Handling
|
|
|
|
# print a canonicalized absolute pathname
|
|
realpath() {
|
|
local path=$1
|
|
|
|
# make path absolute
|
|
local abspath=$path
|
|
if [[ -n ${abspath##/*} ]]; then
|
|
abspath=$(pwd -P)/$abspath
|
|
fi
|
|
|
|
# canonicalize path
|
|
local dirname=
|
|
if [[ -d $abspath ]]; then
|
|
dirname=$(cd "$abspath" && pwd -P)
|
|
abspath=$dirname
|
|
elif [[ -e $abspath ]]; then
|
|
dirname=$(cd "${abspath%/*}/" 2>/dev/null && pwd -P)
|
|
abspath=$dirname/${abspath##*/}
|
|
fi
|
|
|
|
if [[ -d $dirname && -e $abspath ]]; then
|
|
printf '%s\n' "$abspath"
|
|
else
|
|
printf 'invalid path: %s\n' "$path" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# the current git repository's .git directory
|
|
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
|
readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null)
|
|
|
|
# the current git repository's gitattributes file
|
|
readonly CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile)
|
|
if [[ $CORE_ATTRIBUTES ]]; then
|
|
readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
|
|
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
|
|
readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes"
|
|
else
|
|
readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
|
|
fi
|
|
|
|
##### FUNCTIONS
|
|
|
|
# print a message to stderr
|
|
warn() {
|
|
local fmt="$1"
|
|
shift
|
|
# shellcheck disable=SC2059
|
|
printf "transcrypt: $fmt\n" "$@" >&2
|
|
}
|
|
|
|
# print a message to stderr and exit with either
|
|
# the given status or that of the most recent command
|
|
die() {
|
|
local st="$?"
|
|
if [[ "$1" != *[^0-9]* ]]; then
|
|
st="$1"
|
|
shift
|
|
fi
|
|
warn "$@"
|
|
exit "$st"
|
|
}
|
|
|
|
# verify that all requirements have been met
|
|
run_safety_checks() {
|
|
# validate that we're in a git repository
|
|
[[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?'
|
|
|
|
# exit if transcrypt is not in the required state
|
|
if [[ $requires_existing_config ]] && [[ ! $CONFIGURED ]]; then
|
|
die 1 'the current repository is not configured'
|
|
elif [[ ! $requires_existing_config ]] && [[ $CONFIGURED ]]; then
|
|
die 1 'the current repository is already configured; see --display'
|
|
fi
|
|
|
|
# check for dependencies
|
|
for cmd in {column,grep,mktemp,openssl,sed,tee}; do
|
|
command -v $cmd >/dev/null || die 'required command "%s" was not found' "$cmd"
|
|
done
|
|
|
|
# ensure the repository is clean (if it has a HEAD revision) so we can force
|
|
# checkout files without the destruction of uncommitted changes
|
|
if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
|
|
# check if the repo is dirty
|
|
if ! git diff-index --quiet HEAD --; then
|
|
die 1 'the repo is dirty; commit or stash your changes before running transcrypt'
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# unset the cipher variable if it is not supported by openssl
|
|
validate_cipher() {
|
|
local list_cipher_commands
|
|
list_cipher_commands='openssl enc -ciphers'
|
|
remove_dash() {
|
|
sed 's#\(^\| \)-#\1#g'
|
|
}
|
|
|
|
|
|
local supported
|
|
supported=$($list_cipher_commands | remove_dash | tr -s ' ' '\n' | grep --line-regexp "$cipher") || true
|
|
if [[ ! $supported ]]; then
|
|
if [[ $interactive ]]; then
|
|
printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher"
|
|
$list_cipher_commands | remove_dash | column -c 80
|
|
printf '\n'
|
|
cipher=''
|
|
else
|
|
# shellcheck disable=SC2016
|
|
die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$($list_cipher_commands | remove_dash)"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ensure we have a cipher to encrypt with
|
|
get_cipher() {
|
|
while [[ ! $cipher ]]; do
|
|
local answer=
|
|
if [[ $interactive ]]; then
|
|
printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER"
|
|
read -r answer
|
|
fi
|
|
|
|
# use the default cipher if the user gave no answer;
|
|
# otherwise verify the given cipher is supported by openssl
|
|
if [[ ! $answer ]]; then
|
|
cipher=$DEFAULT_CIPHER
|
|
else
|
|
cipher=$answer
|
|
validate_cipher
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ensure we have a password to encrypt with
|
|
get_password() {
|
|
while [[ ! $password ]]; do
|
|
local answer=
|
|
if [[ $interactive ]]; then
|
|
printf 'Generate a random password? [Y/n] '
|
|
read -r -n 1 -s answer
|
|
printf '\n'
|
|
fi
|
|
|
|
# generate a random password if the user answered yes;
|
|
# otherwise prompt the user for a password
|
|
if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
|
|
local password_length=30
|
|
local random_base64
|
|
random_base64=$(openssl rand -base64 $password_length)
|
|
password=$random_base64
|
|
else
|
|
printf 'Password: '
|
|
read -r password
|
|
[[ $password ]] || printf 'no password was specified\n'
|
|
fi
|
|
done
|
|
}
|
|
|
|
# confirm the transcrypt configuration
|
|
confirm_configuration() {
|
|
local answer=
|
|
|
|
printf '\nRepository metadata:\n\n'
|
|
[[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO"
|
|
printf ' GIT_DIR: %s\n' "$GIT_DIR"
|
|
printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
|
|
printf 'The following configuration will be saved:\n\n'
|
|
printf ' CIPHER: %s\n' "$cipher"
|
|
printf ' PASSWORD: %s\n\n' "$password"
|
|
printf 'Does this look correct? [Y/n] '
|
|
read -r -n 1 -s answer
|
|
|
|
# exit if the user did not confirm
|
|
if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
|
|
printf '\n\n'
|
|
else
|
|
printf '\n'
|
|
die 1 'configuration has been aborted'
|
|
fi
|
|
}
|
|
|
|
# confirm the rekey configuration
|
|
confirm_rekey() {
|
|
local answer=
|
|
|
|
printf '\nRepository metadata:\n\n'
|
|
[[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO"
|
|
printf ' GIT_DIR: %s\n' "$GIT_DIR"
|
|
printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
|
|
printf 'The following configuration will be saved:\n\n'
|
|
printf ' CIPHER: %s\n' "$cipher"
|
|
printf ' PASSWORD: %s\n\n' "$password"
|
|
printf 'You are about to re-encrypt all encrypted files using new credentials.\n'
|
|
printf 'Once you do this, their historical diffs will no longer display in plain text.\n\n'
|
|
printf 'Proceed with rekey? [y/N] '
|
|
read -r answer
|
|
|
|
# only rekey if the user explicitly confirmed
|
|
if [[ $answer =~ $YES_REGEX ]]; then
|
|
printf '\n'
|
|
else
|
|
die 1 'rekeying has been aborted'
|
|
fi
|
|
}
|
|
|
|
# automatically stage rekeyed files in preparation for the user to commit them
|
|
stage_rekeyed_files() {
|
|
local encrypted_files
|
|
encrypted_files=$(git ls-crypt)
|
|
if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then
|
|
# touch all encrypted files to prevent stale stat info
|
|
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
|
|
# shellcheck disable=SC2086
|
|
touch $encrypted_files
|
|
# shellcheck disable=SC2086
|
|
git update-index --add -- $encrypted_files
|
|
|
|
printf '*** rekeyed files have been staged ***\n'
|
|
printf '*** COMMIT THESE CHANGES RIGHT AWAY! ***\n\n'
|
|
fi
|
|
}
|
|
|
|
# save helper scripts under the repository's git directory
|
|
save_helper_scripts() {
|
|
mkdir -p "${GIT_DIR}/crypt"
|
|
|
|
openssl_command="openssl enc $ENCRYPT_OPTIONS -pass env:ENC_PASS"
|
|
|
|
# The `decryption -> encryption` process on an unchanged file must be
|
|
# deterministic for everything to work transparently. To do that, the same
|
|
# salt must be used each time we encrypt the same file. An HMAC has been
|
|
# proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file
|
|
# (keyed with a combination of the filename and transcrypt password), and
|
|
# then use the last 16 bytes of that HMAC for the file's unique salt.
|
|
|
|
cat <<-'EOF' >"${GIT_DIR}/crypt/clean"
|
|
#!/usr/bin/env bash
|
|
filename=$1
|
|
# ignore empty files
|
|
if [[ -s $filename ]]; then
|
|
# cache STDIN to test if it's already encrypted
|
|
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
|
|
trap 'rm -f "$tempfile"' EXIT
|
|
tee "$tempfile" &>/dev/null
|
|
# the first bytes of an encrypted file are always "Salted" in Base64
|
|
read -n 8 firstbytes <"$tempfile"
|
|
if [[ $firstbytes == "U2FsdGVk" ]]; then
|
|
cat "$tempfile"
|
|
else
|
|
cipher=$(git config --get --local transcrypt.cipher)
|
|
password=$(git config --get --local transcrypt.password)
|
|
salt=$(openssl dgst -hmac "${filename}:${password}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16)
|
|
ENC_PASS=$password @openssl_command@ -e -a -S "$salt" -in "$tempfile"
|
|
fi
|
|
fi
|
|
EOF
|
|
|
|
cat <<-'EOF' >"${GIT_DIR}/crypt/smudge"
|
|
#!/usr/bin/env bash
|
|
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
|
|
trap 'rm -f "$tempfile"' EXIT
|
|
cipher=$(git config --get --local transcrypt.cipher)
|
|
password=$(git config --get --local transcrypt.password)
|
|
tee "$tempfile" | ENC_PASS=$password @openssl_command@ -d -a 2>/dev/null || cat "$tempfile"
|
|
EOF
|
|
|
|
cat <<-'EOF' >"${GIT_DIR}/crypt/textconv"
|
|
#!/usr/bin/env bash
|
|
filename=$1
|
|
# ignore empty files
|
|
if [[ -s $filename ]]; then
|
|
cipher=$(git config --get --local transcrypt.cipher)
|
|
password=$(git config --get --local transcrypt.password)
|
|
ENC_PASS=$password @openssl_command@ -d -a -in "$filename" 2>/dev/null || cat "$filename"
|
|
fi
|
|
EOF
|
|
|
|
# make scripts executable
|
|
for script in {clean,smudge,textconv}; do
|
|
chmod 0755 "${GIT_DIR}/crypt/${script}"
|
|
sed "s/@openssl_command@/$openssl_command/" -i "${GIT_DIR}/crypt/${script}"
|
|
done
|
|
}
|
|
|
|
# write the configuration to the repository's git config
|
|
save_configuration() {
|
|
save_helper_scripts
|
|
|
|
# write the encryption info
|
|
git config transcrypt.version "$VERSION"
|
|
git config transcrypt.cipher "$cipher"
|
|
git config transcrypt.password "$password"
|
|
|
|
# write the filter settings
|
|
if [[ -d $(git rev-parse --git-common-dir) ]]; then
|
|
# this allows us to support multiple working trees via git-worktree
|
|
# ...but the --git-common-dir flag was only added in November 2014
|
|
# shellcheck disable=SC2016
|
|
git config filter.crypt.clean '"$(git rev-parse --git-common-dir)"/crypt/clean %f'
|
|
# shellcheck disable=SC2016
|
|
git config filter.crypt.smudge '"$(git rev-parse --git-common-dir)"/crypt/smudge'
|
|
# shellcheck disable=SC2016
|
|
git config diff.crypt.textconv '"$(git rev-parse --git-common-dir)"/crypt/textconv'
|
|
else
|
|
# shellcheck disable=SC2016
|
|
git config filter.crypt.clean '"$(git rev-parse --git-dir)"/crypt/clean %f'
|
|
# shellcheck disable=SC2016
|
|
git config filter.crypt.smudge '"$(git rev-parse --git-dir)"/crypt/smudge'
|
|
# shellcheck disable=SC2016
|
|
git config diff.crypt.textconv '"$(git rev-parse --git-dir)"/crypt/textconv'
|
|
fi
|
|
git config filter.crypt.required 'true'
|
|
git config diff.crypt.cachetextconv 'true'
|
|
git config diff.crypt.binary 'true'
|
|
git config merge.renormalize 'true'
|
|
|
|
# add a git alias for listing encrypted files
|
|
git config alias.ls-crypt "!git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = \":\" }; /crypt$/{ print \$1 }'"
|
|
}
|
|
|
|
# display the current configuration settings
|
|
display_configuration() {
|
|
local current_cipher
|
|
current_cipher=$(git config --get --local transcrypt.cipher)
|
|
local current_password
|
|
current_password=$(git config --get --local transcrypt.password)
|
|
local escaped_password=${current_password//\'/\'\\\'\'}
|
|
|
|
printf 'The current repository was configured using transcrypt version %s\n' "$CONFIGURED"
|
|
printf 'and has the following configuration:\n\n'
|
|
[[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO"
|
|
printf ' GIT_DIR: %s\n' "$GIT_DIR"
|
|
printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES"
|
|
printf ' CIPHER: %s\n' "$current_cipher"
|
|
printf ' PASSWORD: %s\n\n' "$current_password"
|
|
printf 'Copy and paste the following command to initialize a cloned repository:\n\n'
|
|
printf " transcrypt -c %s -p '%s'\n" "$current_cipher" "$escaped_password"
|
|
}
|
|
|
|
# remove transcrypt-related settings from the repository's git config
|
|
clean_gitconfig() {
|
|
git config --remove-section transcrypt 2>/dev/null || true
|
|
git config --remove-section filter.crypt 2>/dev/null || true
|
|
git config --remove-section diff.crypt 2>/dev/null || true
|
|
git config --unset merge.renormalize
|
|
|
|
# remove the merge section if it's now empty
|
|
local merge_values
|
|
merge_values=$(git config --get-regex --local 'merge\..*') || true
|
|
if [[ ! $merge_values ]]; then
|
|
git config --remove-section merge 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# force the checkout of any files with the crypt filter applied to them;
|
|
# this will decrypt existing encrypted files if you've just cloned a repository,
|
|
# or it will encrypt locally decrypted files if you've just flushed the credentials
|
|
force_checkout() {
|
|
# make sure a HEAD revision exists
|
|
if [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
|
|
# this would normally delete uncommitted changes in the working directory,
|
|
# but we already made sure the repo was clean during the safety checks
|
|
local encrypted_files
|
|
encrypted_files=$(git ls-crypt)
|
|
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
|
|
IFS=$'\n'
|
|
for file in $encrypted_files; do
|
|
rm "$file"
|
|
git checkout --force HEAD -- "$file" >/dev/null
|
|
done
|
|
unset IFS
|
|
fi
|
|
}
|
|
|
|
# remove the locally cached encryption credentials and
|
|
# re-encrypt any files that had been previously decrypted
|
|
flush_credentials() {
|
|
local answer=
|
|
|
|
if [[ $interactive ]]; then
|
|
printf 'You are about to flush the local credentials; make sure you have saved them elsewhere.\n'
|
|
printf 'All previously decrypted files will revert to their encrypted form.\n\n'
|
|
printf 'Proceed with credential flush? [y/N] '
|
|
read -r answer
|
|
printf '\n'
|
|
else
|
|
# although destructive, we should support the --yes option
|
|
answer='y'
|
|
fi
|
|
|
|
# only flush if the user explicitly confirmed
|
|
if [[ $answer =~ $YES_REGEX ]]; then
|
|
clean_gitconfig
|
|
|
|
# re-encrypt any files that had been previously decrypted
|
|
force_checkout
|
|
|
|
printf 'The local transcrypt credentials have been successfully flushed.\n'
|
|
else
|
|
die 1 'flushing of credentials has been aborted'
|
|
fi
|
|
}
|
|
|
|
# remove all transcrypt configuration from the repository
|
|
uninstall_transcrypt() {
|
|
local answer=
|
|
|
|
if [[ $interactive ]]; then
|
|
printf 'You are about to remove all transcrypt configuration from your repository.\n'
|
|
printf 'All previously encrypted files will remain decrypted in this working copy.\n\n'
|
|
printf 'Proceed with uninstall? [y/N] '
|
|
read -r answer
|
|
printf '\n'
|
|
else
|
|
# although destructive, we should support the --yes option
|
|
answer='y'
|
|
fi
|
|
|
|
# only uninstall if the user explicitly confirmed
|
|
if [[ $answer =~ $YES_REGEX ]]; then
|
|
clean_gitconfig
|
|
|
|
# remove helper scripts
|
|
for script in {clean,smudge,textconv}; do
|
|
[[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}"
|
|
done
|
|
[[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt"
|
|
|
|
# touch all encrypted files to prevent stale stat info
|
|
local encrypted_files
|
|
encrypted_files=$(git ls-crypt)
|
|
if [[ $encrypted_files ]] && [[ $IS_BARE == 'false' ]]; then
|
|
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
|
|
# shellcheck disable=SC2086
|
|
touch $encrypted_files
|
|
fi
|
|
|
|
# remove the `git ls-crypt` alias
|
|
git config --unset alias.ls-crypt
|
|
|
|
# remove the alias section if it's now empty
|
|
local alias_values
|
|
alias_values=$(git config --get-regex --local 'alias\..*') || true
|
|
if [[ ! $alias_values ]]; then
|
|
git config --remove-section alias 2>/dev/null || true
|
|
fi
|
|
|
|
# remove any defined crypt patterns in gitattributes
|
|
case $OSTYPE in
|
|
darwin*)
|
|
/usr/bin/sed -i '' '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
|
|
;;
|
|
linux*)
|
|
sed -i '/filter=crypt diff=crypt[ \t]*$/d' "$GIT_ATTRIBUTES"
|
|
;;
|
|
esac
|
|
|
|
printf 'The transcrypt configuration has been completely removed from the repository.\n'
|
|
else
|
|
die 1 'uninstallation has been aborted'
|
|
fi
|
|
}
|
|
|
|
# list all of the currently encrypted files in the repository
|
|
list_files() {
|
|
if [[ $IS_BARE == 'false' ]]; then
|
|
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
|
|
git ls-files | git check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'
|
|
fi
|
|
}
|
|
|
|
# show the raw file as stored in the git commit object
|
|
show_raw_file() {
|
|
if [[ -f $show_file ]]; then
|
|
# ensure the file is currently being tracked
|
|
local escaped_file=${show_file//\//\\\/}
|
|
if git ls-files --others -- "$show_file" | awk "/${escaped_file}/{ exit 1 }"; then
|
|
file_paths=$(git ls-tree --name-only --full-name HEAD "$show_file")
|
|
else
|
|
die 1 'the file "%s" is not currently being tracked by git' "$show_file"
|
|
fi
|
|
elif [[ $show_file == '*' ]]; then
|
|
file_paths=$(git ls-crypt)
|
|
else
|
|
die 1 'the file "%s" does not exist' "$show_file"
|
|
fi
|
|
|
|
IFS=$'\n'
|
|
for file in $file_paths; do
|
|
printf '==> %s <==\n' "$file" >&2
|
|
git --no-pager show HEAD:"$file" --no-textconv
|
|
printf '\n' >&2
|
|
done
|
|
unset IFS
|
|
}
|
|
|
|
# export password and cipher to a gpg encrypted file
|
|
export_gpg() {
|
|
# check for dependencies
|
|
command -v gpg >/dev/null || die 'required command "gpg" was not found'
|
|
|
|
# ensure the recipient key exists
|
|
if ! gpg --list-keys "$gpg_recipient" 2>/dev/null; then
|
|
die 1 'GPG recipient key "%s" does not exist' "$gpg_recipient"
|
|
fi
|
|
|
|
local current_cipher
|
|
current_cipher=$(git config --get --local transcrypt.cipher)
|
|
local current_password
|
|
current_password=$(git config --get --local transcrypt.password)
|
|
mkdir -p "${GIT_DIR}/crypt"
|
|
|
|
local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -"
|
|
printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${GIT_DIR}/crypt/${gpg_recipient}.asc"
|
|
printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient"
|
|
}
|
|
|
|
# import password and cipher from a gpg encrypted file
|
|
import_gpg() {
|
|
# check for dependencies
|
|
command -v gpg >/dev/null || die 'required command "gpg" was not found'
|
|
|
|
local path
|
|
if [[ -f "${GIT_DIR}/crypt/${gpg_import_file}" ]]; then
|
|
path="${GIT_DIR}/crypt/${gpg_import_file}"
|
|
elif [[ -f "${GIT_DIR}/crypt/${gpg_import_file}.asc" ]]; then
|
|
path="${GIT_DIR}/crypt/${gpg_import_file}.asc"
|
|
elif [[ ! -f $gpg_import_file ]]; then
|
|
die 1 'the file "%s" does not exist' "$gpg_import_file"
|
|
else
|
|
path="$gpg_import_file"
|
|
fi
|
|
|
|
local configuration=''
|
|
local safety_counter=0 # fix for intermittent 'no secret key' decryption failures
|
|
while [[ ! $configuration ]]; do
|
|
configuration=$(gpg --batch --quiet --decrypt "$path")
|
|
|
|
safety_counter=$((safety_counter + 1))
|
|
if [[ $safety_counter -eq 3 ]]; then
|
|
die 1 'unable to decrypt the file "%s"' "$path"
|
|
fi
|
|
done
|
|
|
|
cipher=$(printf '%s' "$configuration" | grep '^cipher' | cut -d'=' -f 2-)
|
|
password=$(printf '%s' "$configuration" | grep '^password' | cut -d'=' -f 2-)
|
|
}
|
|
|
|
# print this script's usage message to stderr
|
|
usage() {
|
|
cat <<-EOF >&2
|
|
usage: transcrypt [-c CIPHER] [-p PASSWORD] [-h]
|
|
EOF
|
|
}
|
|
|
|
# print this script's help message to stdout
|
|
help() {
|
|
cat <<-EOF
|
|
NAME
|
|
transcrypt -- transparently encrypt files within a git repository
|
|
|
|
SYNOPSIS
|
|
transcrypt [options...]
|
|
|
|
DESCRIPTION
|
|
|
|
transcrypt will configure a Git repository to support the transparent
|
|
encryption/decryption of files by utilizing OpenSSL's symmetric cipher
|
|
routines and Git's built-in clean/smudge filters. It will also add a
|
|
Git alias "ls-crypt" to list all transparently encrypted files within
|
|
the repository.
|
|
|
|
The transcrypt source code and full documentation may be downloaded
|
|
from https://github.com/elasticdog/transcrypt.
|
|
|
|
OPTIONS
|
|
-c, --cipher=CIPHER
|
|
the symmetric cipher to utilize for encryption;
|
|
defaults to aes-256-cbc
|
|
|
|
-p, --password=PASSWORD
|
|
the password to derive the key from;
|
|
defaults to 30 random base64 characters
|
|
|
|
-y, --yes
|
|
assume yes and accept defaults for non-specified options
|
|
|
|
-d, --display
|
|
display the current repository's cipher and password
|
|
|
|
-r, --rekey
|
|
re-encrypt all encrypted files using new credentials
|
|
|
|
-f, --flush-credentials
|
|
remove the locally cached encryption credentials and re-encrypt
|
|
any files that had been previously decrypted
|
|
|
|
-F, --force
|
|
ignore whether the git directory is clean, proceed with the
|
|
possibility that uncommitted changes are overwritten
|
|
|
|
-u, --uninstall
|
|
remove all transcrypt configuration from the repository and
|
|
leave files in the current working copy decrypted
|
|
|
|
-l, --list
|
|
list all of the transparently encrypted files in the repository,
|
|
relative to the top-level directory
|
|
|
|
-s, --show-raw=FILE
|
|
show the raw file as stored in the git commit object; use this
|
|
to check if files are encrypted as expected
|
|
|
|
-e, --export-gpg=RECIPIENT
|
|
export the repository's cipher and password to a file encrypted
|
|
for a gpg recipient
|
|
|
|
-i, --import-gpg=FILE
|
|
import the password and cipher from a gpg encrypted file
|
|
|
|
-v, --version
|
|
print the version information
|
|
|
|
-h, --help
|
|
view this help message
|
|
|
|
EXAMPLES
|
|
|
|
To initialize a Git repository to support transparent encryption, just
|
|
change into the repo and run the transcrypt script. transcrypt will
|
|
prompt you interactively for all required information if the corre-
|
|
sponding option flags were not given.
|
|
|
|
$ cd <path-to-your-repo>/
|
|
$ transcrypt
|
|
|
|
Once a repository has been configured with transcrypt, you can trans-
|
|
parently encrypt files by applying the "crypt" filter and diff to a
|
|
pattern in the top-level .gitattributes config. If that pattern matches
|
|
a file in your repository, the file will be transparently encrypted
|
|
once you stage and commit it:
|
|
|
|
$ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes
|
|
$ git add .gitattributes sensitive_file
|
|
$ git commit -m 'Add encrypted version of a sensitive file'
|
|
|
|
See the gitattributes(5) man page for more information.
|
|
|
|
If you have just cloned a repository containing files that are
|
|
encrypted, you'll want to configure transcrypt with the same cipher and
|
|
password as the origin repository. Once transcrypt has stored the
|
|
matching credentials, it will force a checkout of any existing
|
|
encrypted files in order to decrypt them.
|
|
|
|
If the origin repository has just rekeyed, all clones should flush
|
|
their transcrypt credentials, fetch and merge the new encrypted files
|
|
via Git, and then re-configure transcrypt with the new credentials.
|
|
|
|
AUTHOR
|
|
Aaron Bull Schaefer <aaron@elasticdog.com>
|
|
|
|
SEE ALSO
|
|
enc(1), gitattributes(5)
|
|
EOF
|
|
}
|
|
|
|
##### MAIN
|
|
|
|
# reset all variables that might be set
|
|
cipher=''
|
|
password=''
|
|
interactive='true'
|
|
display_config=''
|
|
rekey=''
|
|
flush_creds=''
|
|
uninstall=''
|
|
show_file=''
|
|
gpg_recipient=''
|
|
gpg_import_file=''
|
|
|
|
# used to bypass certain safety checks
|
|
requires_existing_config=''
|
|
requires_clean_repo='true'
|
|
|
|
# parse command line options
|
|
while [[ "${1:-}" != '' ]]; do
|
|
case $1 in
|
|
-c | --cipher)
|
|
cipher=$2
|
|
shift
|
|
;;
|
|
--cipher=*)
|
|
cipher=${1#*=}
|
|
;;
|
|
-p | --password)
|
|
password=$2
|
|
shift
|
|
;;
|
|
--password=*)
|
|
password=${1#*=}
|
|
;;
|
|
-y | --yes)
|
|
interactive=''
|
|
;;
|
|
-d | --display)
|
|
display_config='true'
|
|
requires_existing_config='true'
|
|
requires_clean_repo=''
|
|
;;
|
|
-r | --rekey)
|
|
rekey='true'
|
|
requires_existing_config='true'
|
|
;;
|
|
-f | --flush-credentials)
|
|
flush_creds='true'
|
|
requires_existing_config='true'
|
|
;;
|
|
-F | --force)
|
|
requires_clean_repo=''
|
|
;;
|
|
-u | --uninstall)
|
|
uninstall='true'
|
|
requires_existing_config='true'
|
|
requires_clean_repo=''
|
|
;;
|
|
-l | --list)
|
|
list_files
|
|
exit 0
|
|
;;
|
|
-s | --show-raw)
|
|
show_file=$2
|
|
show_raw_file
|
|
exit 0
|
|
;;
|
|
--show-raw=*)
|
|
show_file=${1#*=}
|
|
show_raw_file
|
|
exit 0
|
|
;;
|
|
-e | --export-gpg)
|
|
gpg_recipient=$2
|
|
requires_existing_config='true'
|
|
requires_clean_repo=''
|
|
shift
|
|
;;
|
|
--export-gpg=*)
|
|
gpg_recipient=${1#*=}
|
|
requires_existing_config='true'
|
|
requires_clean_repo=''
|
|
;;
|
|
-i | --import-gpg)
|
|
gpg_import_file=$2
|
|
shift
|
|
;;
|
|
--import-gpg=*)
|
|
gpg_import_file=${1#*=}
|
|
;;
|
|
-v | --version)
|
|
printf 'transcrypt %s\n' "$VERSION"
|
|
exit 0
|
|
;;
|
|
-h | --help | -\?)
|
|
help
|
|
exit 0
|
|
;;
|
|
--*)
|
|
warn 'unknown option -- %s' "${1#--}"
|
|
usage
|
|
exit 1
|
|
;;
|
|
*)
|
|
warn 'unknown option -- %s' "${1#-}"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# always run our safety checks
|
|
run_safety_checks
|
|
|
|
# in order to keep behavior consistent no matter what order the options were
|
|
# specified in, we must run these here rather than in the case statement above
|
|
if [[ $uninstall ]]; then
|
|
uninstall_transcrypt
|
|
exit 0
|
|
elif [[ $display_config ]] && [[ $flush_creds ]]; then
|
|
display_configuration
|
|
printf '\n'
|
|
flush_credentials
|
|
exit 0
|
|
elif [[ $display_config ]]; then
|
|
display_configuration
|
|
exit 0
|
|
elif [[ $flush_creds ]]; then
|
|
flush_credentials
|
|
exit 0
|
|
elif [[ $gpg_recipient ]]; then
|
|
export_gpg
|
|
exit 0
|
|
elif [[ $gpg_import_file ]]; then
|
|
import_gpg
|
|
elif [[ $cipher ]]; then
|
|
validate_cipher
|
|
fi
|
|
|
|
# perform function calls to configure transcrypt
|
|
get_cipher
|
|
get_password
|
|
|
|
if [[ $rekey ]] && [[ $interactive ]]; then
|
|
confirm_rekey
|
|
elif [[ $interactive ]]; then
|
|
confirm_configuration
|
|
fi
|
|
|
|
save_configuration
|
|
|
|
if [[ $rekey ]]; then
|
|
stage_rekeyed_files
|
|
else
|
|
force_checkout
|
|
fi
|
|
|
|
# ensure the git attributes file exists
|
|
if [[ ! -f $GIT_ATTRIBUTES ]]; then
|
|
mkdir -p "${GIT_ATTRIBUTES%/*}"
|
|
printf '#pattern filter=crypt diff=crypt\n' >"$GIT_ATTRIBUTES"
|
|
fi
|
|
|
|
printf 'The repository has been successfully configured by transcrypt.\n'
|
|
|
|
exit 0
|