docs: add encrypted documents
parent
5d7cac15e6
commit
6bf30632a0
|
@ -0,0 +1,3 @@
|
|||
#pattern filter=crypt diff=crypt
|
||||
docs/biblio/* filter=crypt diff=crypt
|
||||
docs/lectures/* filter=crypt diff=crypt
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,888 @@
|
|||
#!/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
|
Loading…
Reference in New Issue