From 561d158bb79fff95b1c37b195266990c97e6a6c3 Mon Sep 17 00:00:00 2001 From: James Murty Date: Sat, 27 Feb 2021 21:42:36 +1100 Subject: [PATCH] Install entire transcrypt script into repository When initializing transcrypt, copy the whole script file into the repository and call this one script from the Git handler scripts. This approach gives a number of benefits: - The contents of the clean, smudge, textconv, and merge scripts become part of the main script, and thus check-able and lint-able - The clean, smudge, textconv, and merge scripts become trivial, just different invocations of the main script - Longer-term, logic could be moved out of the individual clean, smudge, textconv, and merge scripts into a common function. --- transcrypt | 195 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 115 insertions(+), 80 deletions(-) diff --git a/transcrypt b/transcrypt index 7b47970..ecd92f4 100755 --- a/transcrypt +++ b/transcrypt @@ -108,6 +108,92 @@ 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 + read -rn 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 -c16) + ENC_PASS=$password openssl enc -"$cipher" -md MD5 -pass env:ENC_PASS -e -a -S "$salt" -in "$tempfile" + 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) + tee "$tempfile" | ENC_PASS=$password openssl enc -"$cipher" -md MD5 -pass env:ENC_PASS -d -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) + ENC_PASS=$password openssl enc -"$cipher" -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2>/dev/null || cat "$filename" +} + +# shellcheck disable=SC2005,SC2002,SC2181 +git_merge() { + # Look up name of local branch/ref to which changes are being merged + OURS_LABEL=$(git rev-parse --abbrev-ref HEAD) + # Look up name of the incoming "theirs" branch/ref being merged in. + # TODO There must be a better way of doing this than relying on this reflog + # action environment variable, but I don't know what it is + if [[ "$GIT_REFLOG_ACTION" = "merge "* ]]; then + THEIRS_LABEL=$(echo "$GIT_REFLOG_ACTION" | awk '{print $2}') + fi + if [[ ! "$THEIRS_LABEL" ]]; then + THEIRS_LABEL="theirs" + fi + # Decrypt BASE $1, LOCAL $2, and REMOTE $3 versions of file being merged + echo "$(cat "$1" | ./.git/crypt/smudge)" >"$1" + echo "$(cat "$2" | ./.git/crypt/smudge)" >"$2" + echo "$(cat "$3" | ./.git/crypt/smudge)" >"$3" + # Merge the decrypted files to the temp file named by $2 + git merge-file --marker-size="$4" -L "$OURS_LABEL" -L base -L "$THEIRS_LABEL" "$2" "$1" "$3" + # If the merge was not successful (has conflicts) exit with an error code to + # leave the partially-merged file in place for a manual merge. + if [[ "$?" != "0" ]]; then + exit 1 + fi + # If the merge was successful (no conflicts) re-encrypt the merged temp file $2 + # which git will then update in the index in a following "Auto-merging" step. + # We must explicitly encrypt/clean the file, rather than leave Git to do it, + # because we can otherwise trigger safety check failure errors like: + # error: add_cacheinfo failed to refresh for path 'FILE'; merge aborting. + # To re-encrypt we must first copy the merged file to $5 (the name of the + # working-copy file) so the crypt `clean` script can generate the correct hash + # salt based on the file's real name, instead of the $2 temp file name. + cp "$2" "$5" + # Now we use the `clean` script to encrypt the merged file contents back to the + # temp file $2 where Git expects to find the merge result content. + cat "$5" | ./.git/crypt/clean "$5" >"$2" +} + # verify that all requirements have been met run_safety_checks() { # validate that we're in a git repository @@ -276,103 +362,32 @@ stage_rekeyed_files() { save_helper_scripts() { mkdir -p "${GIT_DIR}/crypt" - # The `decryption -> encryption` process on an unchanged file must be - # deterministic for everything to work transparently. To do that, the same - # salt must be used each time we encrypt the same file. An HMAC has been - # proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file - # (keyed with a combination of the filename and transcrypt password), and - # then use the last 16 bytes of that HMAC for the file's unique salt. + local current_transcrypt + current_transcrypt=$(realpath "$0" 2>/dev/null) + cp "$current_transcrypt" "${GIT_DIR}/crypt/transcrypt" 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 -c16) - ENC_PASS=$password openssl enc -$cipher -md MD5 -pass env:ENC_PASS -e -a -S "$salt" -in "$tempfile" - fi - fi + $(dirname "$0")/transcrypt clean "$@" EOF cat <<-'EOF' >"${GIT_DIR}/crypt/smudge" #!/usr/bin/env bash - tempfile=$(mktemp 2>/dev/null || mktemp -t tmp) - trap 'rm -f "$tempfile"' EXIT - cipher=$(git config --get --local transcrypt.cipher) - password=$(git config --get --local transcrypt.password) - tee "$tempfile" | ENC_PASS=$password openssl enc -$cipher -md MD5 -pass env:ENC_PASS -d -a 2>/dev/null || cat "$tempfile" + $(dirname "$0")/transcrypt smudge "$@" EOF cat <<-'EOF' >"${GIT_DIR}/crypt/textconv" #!/usr/bin/env bash - filename=$1 - # ignore empty files - if [[ -s $filename ]]; then - cipher=$(git config --get --local transcrypt.cipher) - password=$(git config --get --local transcrypt.password) - ENC_PASS=$password openssl enc -$cipher -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2>/dev/null || cat "$filename" - fi + $(dirname "$0")/transcrypt textconv "$@" EOF cat <<-'EOF' >"${GIT_DIR}/crypt/merge" #!/usr/bin/env bash - - # Look up name of local branch/ref to which changes are being merged - OURS_LABEL=$(git rev-parse --abbrev-ref HEAD) - - # Look up name of the incoming "theirs" branch/ref being merged in. - # TODO There must be a better way of doing this than relying on this reflog - # action environment variable, but I don't know what it is - if [[ "$GIT_REFLOG_ACTION" = "merge "* ]]; then - THEIRS_LABEL=$(echo $GIT_REFLOG_ACTION | awk '{print $2}') - fi - if [[ ! "$THEIRS_LABEL" ]]; then - THEIRS_LABEL="theirs" - fi - - # Decrypt BASE $1, LOCAL $2, and REMOTE $3 versions of file being merged - echo "$(cat "$1" | ./.git/crypt/smudge)" > "$1" - echo "$(cat "$2" | ./.git/crypt/smudge)" > "$2" - echo "$(cat "$3" | ./.git/crypt/smudge)" > "$3" - - # Merge the decrypted files to the temp file named by $2 - git merge-file --marker-size=$4 -L "$OURS_LABEL" -L base -L "$THEIRS_LABEL" "$2" "$1" "$3" - - # If the merge was not successful (has conflicts) exit with an error code to - # leave the partially-merged file in place for a manual merge. - if [[ "$?" != "0" ]]; then - exit 1 - fi - - # If the merge was successful (no conflicts) re-encrypt the merged temp file $2 - # which git will then update in the index in a following "Auto-merging" step. - # We must explicitly encrypt/clean the file, rather than leave Git to do it, - # because we can otherwise trigger safety check failure errors like: - # error: add_cacheinfo failed to refresh for path 'FILE'; merge aborting. - - # To re-encrypt we must first copy the merged file to $5 (the name of the - # working-copy file) so the crypt `clean` script can generate the correct hash - # salt based on the file's real name, instead of the $2 temp file name. - cp "$2" "$5" - - # Now we use the `clean` script to encrypt the merged file contents back to the - # temp file $2 where Git expects to find the merge result content. - cat "$5" | ./.git/crypt/clean "$5" > "$2" + $(dirname "$0")/transcrypt merge "$@" EOF # make scripts executable - for script in {clean,smudge,textconv,merge}; do + for script in {transcrypt,clean,smudge,textconv,merge}; do chmod 0755 "${GIT_DIR}/crypt/${script}" done } @@ -637,7 +652,7 @@ uninstall_transcrypt() { fi # remove helper scripts - for script in {clean,smudge,textconv,merge}; do + for script in {transcrypt,clean,smudge,textconv,merge}; do [[ ! -f "${GIT_DIR}/crypt/${script}" ]] || rm "${GIT_DIR}/crypt/${script}" done [[ ! -d "${GIT_DIR}/crypt" ]] || rmdir "${GIT_DIR}/crypt" @@ -982,6 +997,26 @@ 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 $? + ;; -c | --cipher) cipher=$2 shift