secrets/transcrypt: update to 2.2.0
parent
a4e7144fb2
commit
40b7391aba
|
@ -1,3 +1,3 @@
|
|||
#pattern filter=crypt diff=crypt
|
||||
secrets/*/** filter=crypt diff=crypt
|
||||
secrets/default.nix filter=crypt diff=crypt
|
||||
#pattern filter=crypt diff=crypt merge=crypt
|
||||
secrets/*/** filter=crypt diff=crypt merge=crypt
|
||||
secrets/default.nix filter=crypt diff=crypt merge=crypt
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#! nix-shell -i bash --pure
|
||||
#! nix-shell -p bash openssl git unixtools.column
|
||||
#! nix-shell -p bash openssl git unixtools.column perl
|
||||
set -euo pipefail
|
||||
|
||||
#
|
||||
|
@ -18,37 +18,15 @@ set -euo pipefail
|
|||
##### CONSTANTS
|
||||
|
||||
# the release version of this script
|
||||
readonly VERSION='2.0.0'
|
||||
readonly VERSION='2.2.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'
|
||||
# arguments of the openssl enc command
|
||||
readonly ENCRYPT_OPTIONS='-pbkdf2 -iter 200000 -pass env:ENC_PASS'
|
||||
|
||||
# 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() {
|
||||
|
@ -78,21 +56,46 @@ 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
|
||||
# shellcheck disable=SC2155
|
||||
gather_repo_metadata() {
|
||||
# whether or not transcrypt is already configured
|
||||
readonly CONFIGURED=$(git config --get --local transcrypt.version 2>/dev/null)
|
||||
|
||||
# the current git repository's 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)
|
||||
|
||||
##### FUNCTIONS
|
||||
# whether or not a HEAD revision exists
|
||||
readonly HEAD_EXISTS=$(git rev-parse --verify --quiet HEAD 2>/dev/null)
|
||||
|
||||
# https://github.com/RichiH/vcsh
|
||||
# whether or not the git repository is running under vcsh
|
||||
readonly IS_VCSH=$(git config --get --local --bool vcsh.vcsh 2>/dev/null)
|
||||
|
||||
# whether or not the git repository is bare
|
||||
readonly IS_BARE=$(git rev-parse --is-bare-repository 2>/dev/null || printf 'false')
|
||||
|
||||
# the current git repository's .git directory
|
||||
readonly RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '')
|
||||
readonly GIT_DIR=$(realpath "$RELATIVE_GIT_DIR" 2>/dev/null)
|
||||
|
||||
# Respect transcrypt.crypt-dir if present. Default to crypt/ in Git dir
|
||||
readonly CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}")
|
||||
|
||||
# respect core.hooksPath setting, without trailing slash. Fall back to default hooks dir
|
||||
readonly GIT_HOOKS=$(git config core.hooksPath | sed 's:/*$::' 2>/dev/null || printf "%s/hooks" "${RELATIVE_GIT_DIR}")
|
||||
|
||||
# the current git repository's gitattributes file
|
||||
local CORE_ATTRIBUTES
|
||||
CORE_ATTRIBUTES=$(git config --get --local --path core.attributesFile 2>/dev/null || git config --get --path core.attributesFile 2>/dev/null || printf '')
|
||||
if [[ $CORE_ATTRIBUTES ]]; then
|
||||
readonly GIT_ATTRIBUTES=$CORE_ATTRIBUTES
|
||||
elif [[ $IS_BARE == 'true' ]] || [[ $IS_VCSH == 'true' ]]; then
|
||||
readonly GIT_ATTRIBUTES="${GIT_DIR}/info/attributes"
|
||||
else
|
||||
readonly GIT_ATTRIBUTES="${REPO}/.gitattributes"
|
||||
fi
|
||||
}
|
||||
|
||||
# print a message to stderr
|
||||
warn() {
|
||||
|
@ -114,26 +117,209 @@ die() {
|
|||
exit "$st"
|
||||
}
|
||||
|
||||
# The `decryption -> encryption` process on an unchanged file must be
|
||||
# deterministic for everything to work transparently. To do that, the same
|
||||
# salt must be used each time we encrypt the same file. An HMAC has been
|
||||
# proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file
|
||||
# (keyed with a combination of the filename and transcrypt password), and
|
||||
# then use the last 16 bytes of that HMAC for the file's unique salt.
|
||||
|
||||
git_clean() {
|
||||
filename=$1
|
||||
# ignore empty files
|
||||
if [[ ! -s $filename ]]; then
|
||||
return
|
||||
fi
|
||||
# cache STDIN to test if it's already encrypted
|
||||
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
|
||||
trap 'rm -f "$tempfile"' EXIT
|
||||
tee "$tempfile" &>/dev/null
|
||||
# the first bytes of an encrypted file are always "Salted" in Base64
|
||||
# The `head + LC_ALL=C tr` command handles binary data in old and new Bash (#116)
|
||||
firstbytes=$(head -c8 "$tempfile" | LC_ALL=C tr -d '\0')
|
||||
if [[ $firstbytes == "U2FsdGVk" ]]; then
|
||||
cat "$tempfile"
|
||||
else
|
||||
cipher=$(git config --get --local transcrypt.cipher)
|
||||
password=$(git config --get --local transcrypt.password)
|
||||
openssl_path=$(git config --get --local transcrypt.openssl-path)
|
||||
salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16)
|
||||
|
||||
openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1)
|
||||
if [ "$openssl_major_version" -ge "3" ]; then
|
||||
# Encrypt the file to base64, ensuring it includes the prefix 'Salted__' with the salt. #133
|
||||
(
|
||||
echo -n "Salted__" && echo -n "$salt" | perl -pe 's/(..)/chr(hex($1))/ge' &&
|
||||
# Encrypt file to binary ciphertext
|
||||
ENC_PASS="$password" "$openssl_path" enc -e -$cipher $ENCRYPT_OPTIONS -S "$salt" -in "$tempfile"
|
||||
) |
|
||||
openssl base64
|
||||
else
|
||||
# Encrypt file to base64 ciphertext
|
||||
ENC_PASS="$password" "$openssl_path" enc -e -a -$cipher $ENCRYPT_OPTIONS -S "$salt" -in "$tempfile"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
git_smudge() {
|
||||
tempfile=$(mktemp 2>/dev/null || mktemp -t tmp)
|
||||
trap 'rm -f "$tempfile"' EXIT
|
||||
cipher=$(git config --get --local transcrypt.cipher)
|
||||
password=$(git config --get --local transcrypt.password)
|
||||
openssl_path=$(git config --get --local transcrypt.openssl-path)
|
||||
tee "$tempfile" | ENC_PASS="$password" "$openssl_path" enc -d -$cipher $ENCRYPT_OPTIONS -a 2>/dev/null || cat "$tempfile"
|
||||
}
|
||||
|
||||
git_textconv() {
|
||||
filename=$1
|
||||
# ignore empty files
|
||||
if [[ ! -s $filename ]]; then
|
||||
return
|
||||
fi
|
||||
cipher=$(git config --get --local transcrypt.cipher)
|
||||
password=$(git config --get --local transcrypt.password)
|
||||
openssl_path=$(git config --get --local transcrypt.openssl-path)
|
||||
ENC_PASS="$password" "$openssl_path" enc -d -$cipher $ENCRYPT_OPTIONS -a -in "$filename" 2>/dev/null || cat "$filename"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2005,SC2002,SC2181
|
||||
git_merge() {
|
||||
# Get path to transcrypt in this script's directory
|
||||
TRANSCRYPT_PATH="$(dirname "$0")/transcrypt"
|
||||
# Look up name of local branch/ref to which changes are being merged
|
||||
OURS_LABEL=$(git rev-parse --abbrev-ref HEAD)
|
||||
# Look up name of the incoming "theirs" branch/ref being merged in.
|
||||
# TODO There must be a better way of doing this than relying on this reflog
|
||||
# action environment variable, but I don't know what it is
|
||||
if [[ "$GIT_REFLOG_ACTION" = "merge "* ]]; then
|
||||
THEIRS_LABEL=$(echo "$GIT_REFLOG_ACTION" | awk '{print $2}')
|
||||
fi
|
||||
if [[ ! "$THEIRS_LABEL" ]]; then
|
||||
THEIRS_LABEL="theirs"
|
||||
fi
|
||||
# Decrypt BASE $1, LOCAL $2, and REMOTE $3 versions of file being merged
|
||||
echo "$(cat "$1" | "${TRANSCRYPT_PATH}" smudge)" >"$1"
|
||||
echo "$(cat "$2" | "${TRANSCRYPT_PATH}" smudge)" >"$2"
|
||||
echo "$(cat "$3" | "${TRANSCRYPT_PATH}" smudge)" >"$3"
|
||||
# Merge the decrypted files to the temp file named by $2
|
||||
git merge-file --marker-size="$4" -L "$OURS_LABEL" -L base -L "$THEIRS_LABEL" "$2" "$1" "$3"
|
||||
# If the merge was not successful (has conflicts) exit with an error code to
|
||||
# leave the partially-merged file in place for a manual merge.
|
||||
if [[ "$?" != "0" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
# If the merge was successful (no conflicts) re-encrypt the merged temp file $2
|
||||
# which git will then update in the index in a following "Auto-merging" step.
|
||||
# We must explicitly encrypt/clean the file, rather than leave Git to do it,
|
||||
# because we can otherwise trigger safety check failure errors like:
|
||||
# error: add_cacheinfo failed to refresh for path 'FILE'; merge aborting.
|
||||
# To re-encrypt we must first copy the merged file to $5 (the name of the
|
||||
# working-copy file) so the crypt `clean` script can generate the correct hash
|
||||
# salt based on the file's real name, instead of the $2 temp file name.
|
||||
cp "$2" "$5"
|
||||
# Now we use the `clean` script to encrypt the merged file contents back to the
|
||||
# temp file $2 where Git expects to find the merge result content.
|
||||
cat "$5" | "${TRANSCRYPT_PATH}" clean "$5" >"$2"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2155
|
||||
git_pre_commit() {
|
||||
# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64
|
||||
tmp=$(mktemp)
|
||||
IFS=$'\n'
|
||||
slow_mode_if_failed() {
|
||||
for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do
|
||||
# Skip symlinks, they contain the linked target file path not plaintext
|
||||
if [[ -L $secret_file ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get prefix of raw file in Git's index using the :FILENAME revision syntax
|
||||
local firstbytes=$(git show :"${secret_file}" | head -c8)
|
||||
# An empty file does not need to be, and is not, encrypted
|
||||
if [[ $firstbytes == "" ]]; then
|
||||
: # Do nothing
|
||||
# The first bytes of an encrypted file must be "Salted" in Base64
|
||||
elif [[ $firstbytes != "U2FsdGVk" ]]; then
|
||||
printf 'Transcrypt managed file is not encrypted in the Git index: %s\n' "$secret_file" >&2
|
||||
printf '\n' >&2
|
||||
printf 'You probably staged this file using a tool that does not apply' >&2
|
||||
printf ' .gitattribute filters as required by Transcrypt.\n' >&2
|
||||
printf '\n' >&2
|
||||
printf 'Fix this by re-staging the file with a compatible tool or with'
|
||||
printf ' Git on the command line:\n' >&2
|
||||
printf '\n' >&2
|
||||
printf ' git rm --cached -- %s\n' "$secret_file" >&2
|
||||
printf ' git add %s\n' "$secret_file" >&2
|
||||
printf '\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# validate file to see if it failed or not, We don't care about the filename currently for speed, we only care about pass/fail, slow_mode_if_failed() is for what failed.
|
||||
validate_file() {
|
||||
secret_file=${1}
|
||||
# Skip symlinks, they contain the linked target file path not plaintext
|
||||
if [[ -L $secret_file ]]; then
|
||||
return
|
||||
fi
|
||||
# Get prefix of raw file in Git's index using the :FILENAME revision syntax
|
||||
# The first bytes of an encrypted file are always "Salted" in Base64
|
||||
local firstbytes=$(git show :"${secret_file}" | head -c8)
|
||||
if [[ $firstbytes != "U2FsdGVk" ]]; then
|
||||
echo "true" >>"${tmp}"
|
||||
fi
|
||||
}
|
||||
|
||||
# if bash version is 4.4 or greater than fork to number of threads otherwise run normally
|
||||
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]] && [[ "${BASH_VERSINFO[1]}" -ge 4 ]]; then
|
||||
num_procs=$(nproc)
|
||||
num_jobs="\j"
|
||||
for secret_file in $(git -c core.quotePath=false ls-files | git -c core.quotePath=false check-attr --stdin filter | awk 'BEGIN { FS = ":" }; /crypt$/{ print $1 }'); do
|
||||
while ((${num_jobs@P} >= num_procs)); do
|
||||
wait -n
|
||||
done
|
||||
validate_file "${secret_file}" &
|
||||
done
|
||||
wait
|
||||
if [[ -s ${tmp} ]]; then
|
||||
slow_mode_if_failed
|
||||
rm -f "${tmp}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
slow_mode_if_failed
|
||||
fi
|
||||
|
||||
rm -f "${tmp}"
|
||||
unset IFS
|
||||
}
|
||||
|
||||
# verify that all requirements have been met
|
||||
run_safety_checks() {
|
||||
# validate that we're in a git repository
|
||||
[[ $GIT_DIR ]] || die 'you are not currently in a git repository; did you forget to run "git init"?'
|
||||
|
||||
# exit if transcrypt is not in the required state
|
||||
if [[ $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'
|
||||
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"
|
||||
for cmd in {column,grep,mktemp,"${openssl_path}",sed,tee}; do
|
||||
command -v "$cmd" >/dev/null || die 'required command "%s" was not found' "$cmd"
|
||||
done
|
||||
|
||||
# ensure the repository is clean (if it has a HEAD revision) so we can force
|
||||
# checkout files without the destruction of uncommitted changes
|
||||
if [[ $requires_clean_repo ]] && [[ $HEAD_EXISTS ]] && [[ $IS_BARE == 'false' ]]; then
|
||||
# ensure index is up-to-date before dirty check
|
||||
git update-index -q --really-refresh
|
||||
# check if the repo is dirty
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
die 1 'the repo is dirty; commit or stash your changes before running transcrypt'
|
||||
|
@ -143,24 +329,20 @@ run_safety_checks() {
|
|||
|
||||
# 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 list_cipher_commands
|
||||
list_cipher_commands="${openssl_path} enc -ciphers"
|
||||
|
||||
local supported
|
||||
supported=$($list_cipher_commands | remove_dash | tr -s ' ' '\n' | grep --line-regexp "$cipher") || true
|
||||
supported=$($list_cipher_commands | tr -s ' ' '\n' | grep -Fx -- "-$cipher") || true
|
||||
if [[ ! $supported ]]; then
|
||||
if [[ $interactive ]]; then
|
||||
printf '"%s" is not a valid cipher; choose one of the following:\n\n' "$cipher"
|
||||
$list_cipher_commands | remove_dash | column -c 80
|
||||
$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 | remove_dash)"
|
||||
die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
@ -200,7 +382,7 @@ get_password() {
|
|||
if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then
|
||||
local password_length=30
|
||||
local random_base64
|
||||
random_base64=$(openssl rand -base64 $password_length)
|
||||
random_base64=$(${openssl_path} rand -base64 $password_length)
|
||||
password=$random_base64
|
||||
else
|
||||
printf 'Password: '
|
||||
|
@ -276,100 +458,73 @@ stage_rekeyed_files() {
|
|||
|
||||
# save helper scripts under the repository's git directory
|
||||
save_helper_scripts() {
|
||||
mkdir -p "${GIT_DIR}/crypt"
|
||||
mkdir -p "${CRYPT_DIR}"
|
||||
|
||||
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
|
||||
local current_transcrypt
|
||||
current_transcrypt=$(realpath "$0" 2>/dev/null)
|
||||
echo '#!/usr/bin/env bash' > "${CRYPT_DIR}/transcrypt"
|
||||
tail -n +4 "$current_transcrypt" >> "${CRYPT_DIR}/transcrypt"
|
||||
|
||||
# make scripts executable
|
||||
for script in {clean,smudge,textconv}; do
|
||||
chmod 0755 "${GIT_DIR}/crypt/${script}"
|
||||
sed "s/@openssl_command@/$openssl_command/" -i "${GIT_DIR}/crypt/${script}"
|
||||
for script in {transcrypt,}; do
|
||||
chmod 0755 "${CRYPT_DIR}/${script}"
|
||||
done
|
||||
}
|
||||
|
||||
# save helper hooks under the repository's git directory
|
||||
save_helper_hooks() {
|
||||
# Install pre-commit-crypt hook script
|
||||
[[ ! -d "${GIT_HOOKS}" ]] && mkdir -p "${GIT_HOOKS}"
|
||||
pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt"
|
||||
cat <<-'EOF' >"$pre_commit_hook_installed"
|
||||
#!/usr/bin/env bash
|
||||
# Transcrypt pre-commit hook: fail if secret file in staging lacks the magic prefix "Salted" in B64
|
||||
RELATIVE_GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || printf '')
|
||||
CRYPT_DIR=$(git config transcrypt.crypt-dir 2>/dev/null || printf '%s/crypt' "${RELATIVE_GIT_DIR}")
|
||||
"${CRYPT_DIR}/transcrypt" pre_commit
|
||||
EOF
|
||||
|
||||
# Activate hook by copying it to the pre-commit script name, if safe to do so
|
||||
pre_commit_hook="${GIT_HOOKS}/pre-commit"
|
||||
if [[ -f "$pre_commit_hook" ]]; then
|
||||
printf 'WARNING:\n' >&2
|
||||
printf 'Cannot install Git pre-commit hook script because file already exists: %s\n' "$pre_commit_hook" >&2
|
||||
printf 'Please manually install the pre-commit script saved as: %s\n' "$pre_commit_hook_installed" >&2
|
||||
printf '\n'
|
||||
else
|
||||
cp "$pre_commit_hook_installed" "$pre_commit_hook"
|
||||
chmod 0755 "$pre_commit_hook"
|
||||
fi
|
||||
}
|
||||
|
||||
# write the configuration to the repository's git config
|
||||
save_configuration() {
|
||||
save_helper_scripts
|
||||
save_helper_hooks
|
||||
|
||||
# write the encryption info
|
||||
git config transcrypt.version "$VERSION"
|
||||
git config transcrypt.cipher "$cipher"
|
||||
git config transcrypt.password "$password"
|
||||
git config transcrypt.openssl-path "$openssl_path"
|
||||
|
||||
# write the filter settings
|
||||
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
|
||||
# write the filter settings. Sorry for the horrific quote escaping below...
|
||||
# shellcheck disable=SC2016
|
||||
git config filter.crypt.clean '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt clean %f'
|
||||
# shellcheck disable=SC2016
|
||||
git config filter.crypt.smudge '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt smudge'
|
||||
# shellcheck disable=SC2016
|
||||
git config diff.crypt.textconv '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt textconv'
|
||||
# shellcheck disable=SC2016
|
||||
git config merge.crypt.driver '"$(git config transcrypt.crypt-dir 2>/dev/null || printf ''%s/crypt'' ""$(git rev-parse --git-dir)"")"/transcrypt merge %O %A %B %L %P'
|
||||
git config filter.crypt.required 'true'
|
||||
git config diff.crypt.cachetextconv 'true'
|
||||
git config diff.crypt.binary 'true'
|
||||
git config merge.renormalize 'true'
|
||||
git config merge.crypt.name 'Merge transcrypt secret files'
|
||||
|
||||
# add a git alias for listing encrypted files
|
||||
git config alias.ls-crypt "!git 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
|
||||
|
@ -396,6 +551,7 @@ 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
|
||||
|
@ -406,6 +562,20 @@ clean_gitconfig() {
|
|||
fi
|
||||
}
|
||||
|
||||
# Remove from the local Git DB any objects containing the cached plaintext of
|
||||
# secret files, created due to the setting diff.crypt.cachetextconv='true'
|
||||
remove_cached_plaintext() {
|
||||
# Delete ref to cached plaintext objects, to leave these objects
|
||||
# unreferenced and available for removal
|
||||
git update-ref -d refs/notes/textconv/crypt
|
||||
|
||||
# Remove ANY unreferenced objects in Git's object DB (packed or unpacked),
|
||||
# to ensure that cached plaintext objects are also removed.
|
||||
# The vital sub-commands equivalents we require this `gc` command to do are:
|
||||
# `git prune`, `git repack -ad`
|
||||
git gc --prune=now --quiet
|
||||
}
|
||||
|
||||
# force the checkout of any files with the crypt filter applied to them;
|
||||
# this will decrypt existing encrypted files if you've just cloned a repository,
|
||||
# or it will encrypt locally decrypted files if you've just flushed the credentials
|
||||
|
@ -419,7 +589,7 @@ force_checkout() {
|
|||
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
|
||||
IFS=$'\n'
|
||||
for file in $encrypted_files; do
|
||||
rm "$file"
|
||||
rm -f "$file"
|
||||
git checkout --force HEAD -- "$file" >/dev/null
|
||||
done
|
||||
unset IFS
|
||||
|
@ -433,7 +603,8 @@ flush_credentials() {
|
|||
|
||||
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 'All previously decrypted files will revert to their encrypted form, and your\n'
|
||||
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n'
|
||||
printf 'Proceed with credential flush? [y/N] '
|
||||
read -r answer
|
||||
printf '\n'
|
||||
|
@ -446,6 +617,8 @@ flush_credentials() {
|
|||
if [[ $answer =~ $YES_REGEX ]]; then
|
||||
clean_gitconfig
|
||||
|
||||
remove_cached_plaintext
|
||||
|
||||
# re-encrypt any files that had been previously decrypted
|
||||
force_checkout
|
||||
|
||||
|
@ -461,7 +634,8 @@ uninstall_transcrypt() {
|
|||
|
||||
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 'All previously encrypted files will remain decrypted in this working copy, but your\n'
|
||||
printf 'repo will be garbage collected to remove any cached plaintext of secret files.\n\n'
|
||||
printf 'Proceed with uninstall? [y/N] '
|
||||
read -r answer
|
||||
printf '\n'
|
||||
|
@ -474,11 +648,30 @@ uninstall_transcrypt() {
|
|||
if [[ $answer =~ $YES_REGEX ]]; then
|
||||
clean_gitconfig
|
||||
|
||||
if [[ ! $upgrade ]]; then
|
||||
remove_cached_plaintext
|
||||
fi
|
||||
|
||||
# remove helper scripts
|
||||
for script in {clean,smudge,textconv}; do
|
||||
[[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}"
|
||||
# Keep obsolete clean,smudge,textconv,merge refs here to remove them on upgrade
|
||||
for script in {transcrypt,clean,smudge,textconv,merge}; do
|
||||
[[ ! -f "${CRYPT_DIR}/${script}" ]] || rm "${CRYPT_DIR}/${script}"
|
||||
done
|
||||
[[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt"
|
||||
[[ ! -d "${CRYPT_DIR}" ]] || rmdir "${CRYPT_DIR}"
|
||||
|
||||
# rename helper hooks (don't delete, in case user has custom changes)
|
||||
pre_commit_hook="${GIT_HOOKS}/pre-commit"
|
||||
pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt"
|
||||
if [[ -f "$pre_commit_hook" ]]; then
|
||||
hook_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook")
|
||||
installed_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook_installed")
|
||||
if [[ "$hook_md5" = "$installed_md5" ]]; then
|
||||
rm "$pre_commit_hook"
|
||||
else
|
||||
printf 'WARNING: Cannot safely disable Git pre-commit hook %s please check it yourself\n' "$pre_commit_hook"
|
||||
fi
|
||||
fi
|
||||
[[ -f "$pre_commit_hook_installed" ]] && rm "$pre_commit_hook_installed"
|
||||
|
||||
# touch all encrypted files to prevent stale stat info
|
||||
local encrypted_files
|
||||
|
@ -503,23 +696,85 @@ uninstall_transcrypt() {
|
|||
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'
|
||||
if [[ ! $upgrade ]]; then
|
||||
printf 'The transcrypt configuration has been completely removed from the repository.\n'
|
||||
fi
|
||||
else
|
||||
die 1 'uninstallation has been aborted'
|
||||
fi
|
||||
}
|
||||
|
||||
# uninstall and re-install transcrypt to upgrade scripts and update configuration
|
||||
upgrade_transcrypt() {
|
||||
CURRENT_VERSION=$(git config --get --local transcrypt.version 2>/dev/null)
|
||||
|
||||
if [[ $interactive ]]; then
|
||||
printf 'You are about to upgrade the transcrypt scripts in your repository.\n'
|
||||
printf 'Your configuration settings will not be changed.\n\n'
|
||||
printf ' Current version: %s\n' "$CURRENT_VERSION"
|
||||
printf 'Upgraded version: %s\n\n' "$VERSION"
|
||||
printf 'Proceed with upgrade? [y/N] '
|
||||
read -r answer
|
||||
printf '\n'
|
||||
|
||||
if [[ $answer =~ $YES_REGEX ]]; then
|
||||
# User confirmed, don't prompt again
|
||||
interactive=''
|
||||
else
|
||||
# User did not confirm, exit
|
||||
# Exit if user did not confirm
|
||||
die 1 'upgrade has been aborted'
|
||||
fi
|
||||
fi
|
||||
|
||||
# Keep current cipher and password
|
||||
cipher=$(git config --get --local transcrypt.cipher)
|
||||
password=$(git config --get --local transcrypt.password)
|
||||
# Keep current openssl-path, or set to default if no existing value
|
||||
openssl_path=$(git config --get --local transcrypt.openssl-path 2>/dev/null || printf '%s' "$openssl_path")
|
||||
|
||||
# Keep contents of .gitattributes
|
||||
ORIG_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES")
|
||||
|
||||
uninstall_transcrypt
|
||||
save_configuration
|
||||
|
||||
# Re-instate contents of .gitattributes
|
||||
echo "$ORIG_GITATTRIBUTES" >"$GIT_ATTRIBUTES"
|
||||
|
||||
# Update .gitattributes for transcrypt'ed files to include "merge=crypt" config
|
||||
case $OSTYPE in
|
||||
darwin*)
|
||||
/usr/bin/sed -i '' 's/=crypt\(.*\)/=crypt diff=crypt merge=crypt/' "$GIT_ATTRIBUTES"
|
||||
;;
|
||||
linux*)
|
||||
sed -i 's/=crypt\(.*\)/=crypt diff=crypt merge=crypt/' "$GIT_ATTRIBUTES"
|
||||
;;
|
||||
esac
|
||||
|
||||
printf 'Upgrade is complete\n'
|
||||
|
||||
LATEST_GITATTRIBUTES=$(cat "$GIT_ATTRIBUTES")
|
||||
if [[ "$LATEST_GITATTRIBUTES" != "$ORIG_GITATTRIBUTES" ]]; then
|
||||
printf '\nYour gitattributes file has been updated with the latest recommended values.\n'
|
||||
printf 'Please review and commit the new values in:\n'
|
||||
printf '%s\n' "$GIT_ATTRIBUTES"
|
||||
fi
|
||||
}
|
||||
|
||||
# list all of the currently encrypted files in the repository
|
||||
list_files() {
|
||||
if [[ $IS_BARE == 'false' ]]; then
|
||||
cd "$REPO" || die 1 'could not change into the "%s" directory' "$REPO"
|
||||
git 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
|
||||
}
|
||||
|
||||
|
@ -528,8 +783,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
|
||||
|
@ -562,10 +817,10 @@ export_gpg() {
|
|||
current_cipher=$(git config --get --local transcrypt.cipher)
|
||||
local current_password
|
||||
current_password=$(git config --get --local transcrypt.password)
|
||||
mkdir -p "${GIT_DIR}/crypt"
|
||||
mkdir -p "${CRYPT_DIR}"
|
||||
|
||||
local gpg_encrypt_cmd="gpg --batch --recipient $gpg_recipient --trust-model always --yes --armor --quiet --encrypt -"
|
||||
printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${GIT_DIR}/crypt/${gpg_recipient}.asc"
|
||||
printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${CRYPT_DIR}/${gpg_recipient}.asc"
|
||||
printf "The transcrypt configuration has been encrypted and exported to:\n%s/crypt/%s.asc\n" "$GIT_DIR" "$gpg_recipient"
|
||||
}
|
||||
|
||||
|
@ -575,10 +830,10 @@ import_gpg() {
|
|||
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"
|
||||
if [[ -f "${CRYPT_DIR}/${gpg_import_file}" ]]; then
|
||||
path="${CRYPT_DIR}/${gpg_import_file}"
|
||||
elif [[ -f "${CRYPT_DIR}/${gpg_import_file}.asc" ]]; then
|
||||
path="${CRYPT_DIR}/${gpg_import_file}.asc"
|
||||
elif [[ ! -f $gpg_import_file ]]; then
|
||||
die 1 'the file "%s" does not exist' "$gpg_import_file"
|
||||
else
|
||||
|
@ -636,6 +891,9 @@ help() {
|
|||
the password to derive the key from;
|
||||
defaults to 30 random base64 characters
|
||||
|
||||
--set-openssl-path=PATH_TO_OPENSSL
|
||||
use OpenSSL at this path; defaults to 'openssl' in \$PATH
|
||||
|
||||
-y, --yes
|
||||
assume yes and accept defaults for non-specified options
|
||||
|
||||
|
@ -657,6 +915,10 @@ help() {
|
|||
remove all transcrypt configuration from the repository and
|
||||
leave files in the current working copy decrypted
|
||||
|
||||
--upgrade
|
||||
apply the latest transcrypt scripts in the repository without
|
||||
changing your configuration settings
|
||||
|
||||
-l, --list
|
||||
list all of the transparently encrypted files in the repository,
|
||||
relative to the top-level directory
|
||||
|
@ -689,12 +951,12 @@ help() {
|
|||
$ 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:
|
||||
parently encrypt files by applying the "crypt" filter, diff and merge
|
||||
to a pattern in the top-level .gitattributes config. If that pattern
|
||||
matches a file in your repository, the file will be transparently
|
||||
encrypted once you stage and commit it:
|
||||
|
||||
$ echo 'sensitive_file filter=crypt diff=crypt' >> .gitattributes
|
||||
$ 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'
|
||||
|
||||
|
@ -722,23 +984,52 @@ help() {
|
|||
|
||||
# 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=''
|
||||
upgrade=''
|
||||
openssl_path='openssl'
|
||||
|
||||
# used to bypass certain safety checks
|
||||
requires_existing_config=''
|
||||
requires_clean_repo='true'
|
||||
ignore_config_status='' # Set for operations where config can exist or not
|
||||
|
||||
# parse command line options
|
||||
while [[ "${1:-}" != '' ]]; do
|
||||
case $1 in
|
||||
clean)
|
||||
shift
|
||||
git_clean "$@"
|
||||
exit $?
|
||||
;;
|
||||
smudge)
|
||||
shift
|
||||
git_smudge "$@"
|
||||
exit $?
|
||||
;;
|
||||
textconv)
|
||||
shift
|
||||
git_textconv "$@"
|
||||
exit $?
|
||||
;;
|
||||
merge)
|
||||
shift
|
||||
git_merge "$@"
|
||||
exit $?
|
||||
;;
|
||||
pre_commit)
|
||||
shift
|
||||
git_pre_commit "$@"
|
||||
exit $?
|
||||
;;
|
||||
-c | --cipher)
|
||||
cipher=$2
|
||||
shift
|
||||
|
@ -753,6 +1044,11 @@ while [[ "${1:-}" != '' ]]; do
|
|||
--password=*)
|
||||
password=${1#*=}
|
||||
;;
|
||||
--set-openssl-path=*)
|
||||
openssl_path=${1#*=}
|
||||
# Immediately apply config setting
|
||||
git config transcrypt.openssl-path "$openssl_path"
|
||||
;;
|
||||
-y | --yes)
|
||||
interactive=''
|
||||
;;
|
||||
|
@ -777,9 +1073,15 @@ while [[ "${1:-}" != '' ]]; do
|
|||
requires_existing_config='true'
|
||||
requires_clean_repo=''
|
||||
;;
|
||||
--upgrade)
|
||||
upgrade='true'
|
||||
requires_existing_config='true'
|
||||
requires_clean_repo=''
|
||||
;;
|
||||
-l | --list)
|
||||
list_files
|
||||
exit 0
|
||||
list='true'
|
||||
requires_clean_repo=''
|
||||
ignore_config_status='true'
|
||||
;;
|
||||
-s | --show-raw)
|
||||
show_file=$2
|
||||
|
@ -831,14 +1133,25 @@ while [[ "${1:-}" != '' ]]; do
|
|||
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 [[ $upgrade ]]; then
|
||||
upgrade_transcrypt
|
||||
exit 0
|
||||
elif [[ $display_config ]] && [[ $flush_creds ]]; then
|
||||
display_configuration
|
||||
printf '\n'
|
||||
|
@ -880,7 +1193,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'
|
||||
|
|
Loading…
Reference in New Issue