From 20263c8e887d9842a08273e116cfd842fee72dc0 Mon Sep 17 00:00:00 2001 From: joncrall Date: Mon, 25 Apr 2022 22:06:55 -0400 Subject: [PATCH 01/15] Rebase my patch onto main in 2022 --- transcrypt | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/transcrypt b/transcrypt index 0319f48..a0a019c 100755 --- a/transcrypt +++ b/transcrypt @@ -120,39 +120,60 @@ die() { # then use the last 16 bytes of that HMAC for the file's unique salt. git_clean() { + + # The clean script encrypts files before git sends them to the remote. + # Note the "Salted" check is part of openssl and not anything we do here. + # It allows anyone (including us) to check if a file was already encrypted + # but this does compromise the encrypted stream of data (which starts on + # the 17th byte). + # References: https://crypto.stackexchange.com/questions/8776/what-is-u2fsdgvkx1 filename=$1 # ignore empty files if [[ ! -s $filename ]]; then return fi # cache STDIN to test if it's already encrypted + # First, create the tempfile, then + # set a trap to remove the tempfile when we exit or if anything goes wrong + # finally write the stdin of this script to the tempfile 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) + # this is an openssl standard. The actual encrypted stream starts on the 17th byte. firstbytes=$(head -c8 "$tempfile" | LC_ALL=C tr -d '\0') if [[ $firstbytes == "U2FsdGVk" ]]; then + # The file is already encrypted, so just pass it back 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) - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -e -a -S "$salt" -in "$tempfile" + #salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16) + #ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -e -a -S "$salt" -in "$tempfile" + # NOTE: salt must be 16 bytes, its openssl standard + salt=$("$openssl_path" dgst -hmac "${filename}:${password}" -sha512 "$filename" | tr -d '\r\n' | tail -c 16) + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md SHA512 -pass env:ENC_PASS -pbkdf2 -e -a -S "$salt" -in "$tempfile" fi } git_smudge() { + # The smudge script decrypts files when they are checked out by an authenticated repository. + # the file contents are passed via stdin 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 "-${cipher}" -md MD5 -pass env:ENC_PASS -d -a 2>/dev/null || cat "$tempfile" + #tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -d -a 2>/dev/null || cat "$tempfile" + tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md SHA512 -pass env:ENC_PASS -pbkdf2 -d -a 2>/dev/null || cat "$tempfile" } git_textconv() { + # The textconv script allows users to see git diffs in plaintext. + # It does this by decrypting the encrypted git globs into plain text before + # passing them to the diff command. filename=$1 # ignore empty files if [[ ! -s $filename ]]; then @@ -161,7 +182,8 @@ git_textconv() { 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 "-${cipher}" -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2>/dev/null || cat "$filename" + #ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2>/dev/null || cat "$filename" + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md SHA512 -pass env:ENC_PASS -pbkdf2 -d -a -in "$filename" 2>/dev/null || cat "$filename" } # shellcheck disable=SC2005,SC2002,SC2181 From c37642eb861bcf4c00beb5fcfb09c25329d9aa80 Mon Sep 17 00:00:00 2001 From: joncrall Date: Mon, 25 Apr 2022 22:15:15 -0400 Subject: [PATCH 02/15] Make my options configurable --- .gitattributes | 1 - bash_helpers.sh | 240 +++++++++++++++++ example/end_to_end_example.sh | 51 ++++ sensitive_file | Bin 2560 -> 4416 bytes tests/local_test.sh | 6 + transcrypt | 468 ++++++++++++++++++++++++++++------ 6 files changed, 694 insertions(+), 72 deletions(-) create mode 100644 bash_helpers.sh create mode 100644 example/end_to_end_example.sh create mode 100644 tests/local_test.sh diff --git a/.gitattributes b/.gitattributes index 7c479b7..e69de29 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +0,0 @@ -sensitive_file filter=crypt diff=crypt merge=crypt diff --git a/bash_helpers.sh b/bash_helpers.sh new file mode 100644 index 0000000..f3c3252 --- /dev/null +++ b/bash_helpers.sh @@ -0,0 +1,240 @@ +#!/usr/bin/env bash + +_is_contained_str(){ + __doc__=' + Args: + arg : the query to check if it is contained in the values + values : a string of space separated values + + Example: + source ~/code/transcrypt/bash_helpers.sh + # Demo using raw call + (_is_contained_str "foo" "foo bar baz" && echo "contained") || echo "missing" + (_is_contained_str "bar" "foo bar baz" && echo "contained") || echo "missing" + (_is_contained_str "baz" "foo bar baz" && echo "contained") || echo "missing" + (_is_contained_str "biz" "foo bar baz" && echo "contained") || echo "missing" + # Demo using variables + arg="bar" + values="foo bar baz" + (_is_contained_str "$arg" "$values" && echo "contained") || echo "missing" + ' + arg=$1 + values=$2 + echo "$values" | tr -s ' ' '\n' | grep -Fx "$arg" &>/dev/null +} + +_is_contained_arr(){ + __doc__=' + Check if the first value is contained the rest of the values + + Args: + arg : the query to check if it is contained in the values + *values : the rest of the arguments are individual elements in the values + + Example: + source ~/code/transcrypt/bash_helpers.sh + # Demo using raw call + (_is_contained_arr "bar" "foo" "bar" "baz" && echo "contained") || echo "missing" + (_is_contained_arr "biz" "foo" "bar" "baz" && echo "contained") || echo "missing" + # Demo using variables + values=("foo" "bar" "baz") + arg="bar" + (_is_contained_arr "$arg" "${values[@]}" && echo "contained") || echo "missing" + arg="biz" + (_is_contained_arr "$arg" "${values[@]}" && echo "contained") || echo "missing" + ' + # The first argument must be equal to one of the subsequent arguments + local arg=$1 + shift + local arr=("$@") + for val in "${arr[@]}"; + do + if [[ "${arg}" == "${val}" ]]; then + return 0 + fi + done + return 1 +} + +_benchmark_methods(){ + arg="sha512" + source ~/code/transcrypt/bash_helpers.sh + time (openssl list -digest-commands | tr -s ' ' '\n' | grep -Fx "$arg") + echo $? + time _is_contained_str "$arg" "$(openssl list -digest-commands)" + echo $? + time (readarray -t available <<< "$(openssl list -digest-commands | tr -s ' ' '\n')" && _is_contained_arr "$arg" "${available[@]}") + echo $? + #bash_array_repr "${available[@]}" +} + + +joinby(){ + __doc__=' + A function that works similar to a Python join + + Args: + SEP: the separator + *ARR: elements of the strings to join + + Usage: + source $HOME/local/init/utils.sh + ARR=("foo" "bar" "baz") + RESULT=$(joinby / "${ARR[@]}") + echo "RESULT = $RESULT" + + RESULT = foo/bar/baz + + References: + https://stackoverflow.com/questions/1527049/how-can-i-join-elements-of-an-array-in-bash + ' + _handle_help "$@" || return 0 + local d=${1-} f=${2-} + if shift 2; then + printf %s "$f" "${@/#/$d}" + fi +} + +_set_global(){ + # sets a bash global variable by name + key=$1 + val=$2 + printf -v "$key" '%s' "$val" +} + +_validate_variable_arr(){ + __doc__=' + Example: + source bash_helpers.sh + foo="bar" + valid_values=("bar" "biz") + _validate_variable "foo" "${valid_values[@]}" + interactive=1 + _validate_variable "blaz" "${valid_values[@]}" + ' + local varname=$1 + local valid_values=$2 + local varval=${!varname} + if ! _is_contained_arr "$varval" "${valid_values[@]}"; then + local valid_values_str + valid_values_str=$(joinby ', ' "${valid_values[@]}") + message=$(printf "%s is %s, but must be one of: %s" "$varname" "$varval" "$valid_values_str") + if [[ $interactive ]]; then + _set_global "$varname" "" + echo "$message" + else + die 1 "$message" + fi + fi +} + + +_validate_variable_str(){ + __doc__=' + Checks if the target variable is in the set of valid values. + If it is not, it unsets the target variable, then if not in interactive + mode it calls die. + + Args: + varname: name of variable to validate + valid_values: space separated string of valid values + + Example: + source bash_helpers.sh + valid_values="bar biz" + foo="bar" + _validate_variable_str "foo" "$valid_values" + interactive=1 + blaz=fds + _validate_variable_str "blaz" "$valid_values" + ' + local varname=$1 + local valid_values=$2 + local varval=${!varname} + if ! _is_contained_str "$varval" "$valid_values"; then + message=$(printf '%s is `%s`, but must be one of: %s' "$varname" "$varval" "$valid_values") + if [[ $interactive ]]; then + _set_global "$varname" "" + echo "$message" + else + die 1 "$message" + fi + fi +} + +_get_user_input2() { + __doc__=' + Helper to prompt the user, store a response, and validate the result + Args: + varname : name of the bash variable to populate + default : the default value to use if the user provides no answer + valid_values: space separated string of valid values + prompt : string to present to the user + + Example: + source ~/code/transcrypt/bash_helpers.sh + interactive=1 + myvar= + echo "myvar = <$myvar>" + _get_user_input2 "myvar" "a" "a b c" "choose one" + ' + local varname=$1 + local default=$2 + local valid_values=$3 + local prompt=$4 + + while [[ ! ${!varname} ]]; do + local answer= + if [[ $interactive ]]; then + printf '%s > ' "$prompt" + read -r answer + fi + # use the default value if the user gave no answer; otherwise call the + # validate function, which should set the varname to empty if it is + # invalid and the user should continue, otherwise it should die. + if [[ ! $answer ]]; then + _set_global "$varname" "$default" + else + _set_global "$varname" "$answer" + _validate_variable_str "$varname" "$valid_values" + fi + done +} + +_openssl_list(){ + # Args: the openssl commands to list + __doc__=' + source ~/code/transcrypt/bash_helpers.sh + arg=digest-commands + _openssl_list digest-commands + _openssl_list cipher-commands + ' + openssl_path=openssl + arg=$1 + if "${openssl_path} list-$arg" &>/dev/null; then + # OpenSSL < v1.1.0 + "${openssl_path}" "list-$arg" + else + # OpenSSL >= v1.1.0 + "${openssl_path}" "list" "-$arg" + fi +} + + +# shellcheck disable=SC2155 +_check_config_poc(){ + # Notes on custom config + # https://unix.stackexchange.com/questions/175648/use-config-file-for-my-shell-script + mkdir -p "${VERSIONED_CONFIG_DPATH}" + touch "${VERSIONED_TC_CONFIG}" + git config -f "$VERSIONED_TC_CONFIG" --get transcrypt.cipher + git config -f "$VERSIONED_TC_CONFIG" --get transcrypt.rotating.salt + + # POC for using git to store cross-checkout configs + extra_salt=$(openssl rand -hex 32) + git config --file "${VERSIONED_TC_CONFIG}" transcrypt.cipher "aes-256-cbc" + git config --file "${VERSIONED_TC_CONFIG}" transcrypt.use-pbkdf2 "true" --type=bool + git config --file "${VERSIONED_TC_CONFIG}" transcrypt.digest "SHA512" + git config --file "${VERSIONED_TC_CONFIG}" transcrypt.salt-method "auto" + git config --file "${VERSIONED_TC_CONFIG}" transcrypt.extra-salt "${extra_salt}" +} diff --git a/example/end_to_end_example.sh b/example/end_to_end_example.sh new file mode 100644 index 0000000..dab84fa --- /dev/null +++ b/example/end_to_end_example.sh @@ -0,0 +1,51 @@ +#!/bin/bash +__doc__=" +A simple demo of transcrypt +" + +TMP_DIR=$HOME/tmp/transcrypt-demo +mkdir -p "$TMP_DIR" +rm -rf "$TMP_DIR" + + + +# Make a git repo and add some public content +DEMO_REPO=$TMP_DIR/repo +mkdir -p "$DEMO_REPO" +cd "$DEMO_REPO" +git init +echo "content" > README.md +git add README.md +git commit -m "add readme" + + +# Create safe directory that we will encrypt +echo " +safe/* filter=crypt diff=crypt merge=crypt +" > .gitattributes +git add .gitattributes +git commit -m "add attributes" + +mkdir -p "$DEMO_REPO"/safe + + +# Configure transcrypt with legacy defaults +transcrypt -c aes-256-cbc -p 'correct horse battery staple' -md MD5 --use-pbkdf2=0 -sm password -y + +echo "Secret contents" > "$DEMO_REPO"/safe/secret_file +cat "$DEMO_REPO"/safe/secret_file + +git add safe/secret_file +git commit -m "add secret with config1" +transcrypt -s safe/secret_file + + +# Rekey with more secure settings +transcrypt --rekey -c aes-256-cbc -p 'correct horse battery staple' -md SHA256 --use-pbkdf2=1 -sm password -y +git commit -am "changed crypto settings" + + +echo "New secret contents" >> "$DEMO_REPO"/safe/secret_file +git commit -am "added secrets" + +transcrypt -f -y diff --git a/sensitive_file b/sensitive_file index 547ad712f600ee2864e75bf539b829a720b40c0f..a5ab7361c0873fd1e1501b09325a53343b862ec1 100644 GIT binary patch delta 1877 zcmV-b2demh6u=^Xbcw7t47}5{fG21==F~0>LRisY47}y7ej8V9S6-{4GL2Lt01U)O zZF+aYG5AtkL<2+{2O?5|{>oj47NS#&0&g#R=Zxt#3squlpU22+UHek*?DnP1D6qeq zGKULGtr%URD5m{clGC}qFqt>4^Om5DY5oaF?7Em+RAozl=OEUM_1F>ERRB)vg8Z*k zhFJ3WJCwwwaB4CcE@wUxzX3lAXj^Vvge64WqDBh^yPcx7xASq%-S)IW9uQL*a4SzS;B>327P zYM9*DdiwXPU)+liyu|q( zZEvK1Nb8-&XlZi#l1Cj55K0~vx7Dw}!*lXX4_gNMWxw*ArI}tADAv=_XKICOD%(6g z{Y$g|Q3|66hR-0A5|Qry2NBPP3a@)ZPPB&S4%kvG4ad}2V*$bZD79)K91lH4xXLgv zrAao1rc|G;nH$Hz3Ls94mXZ5^jGb$KAbPETM+kpP^(oKEcKGteYCZ;Qwil%z`>g*- z(PHOmbo?%({?3f#c+v1vZ6I^tE_LPfDaSn2;amI9&R@$0q+z~L_*PJM7%zpv6oDoo z#DwSdg<<4&!_v%uz+4arM1pl&y+Xab{X{dy2-QTwSL!)Xlhad4)hS)1gSqgFp%xr} zh(Gq46cj8@>Hk?Bdq-chP0NvH}fu zcmky`5ki7%lUF6Tv)IF1t>&M%!JIivK2djB+4`pPvk9K3%j&w$&*e;!(W4skg}q(3 zbEXa=JW7@;z}vh!lBCiDUzAVa=d!+kbw1g+uN%xB3X0wzZW_dm6nN5={EFKN{9)2so&!7{#!EjKw#6 zT~^aG;gH_B@vIcrf&4-t&ifas6`sf5YoeK~K?dqS@H#~}bm^Oj)oTbmn1wj^->^lR z;s5+Na$mRjH9J*Hj?^i;@ydH~>fa;5vi;PLZgx~C`m3MMqKDvxmV=Z%hFErc;%qxn zz_^zKKAao14_iIBc2dD-2bDyBFYQ-TM#v;bXII%(vx_W3<>P5Z|D|eYX}f=aMm zh%O+09=~0(3b3o=WH6a%sNLK4m%H9t%CYpUlh1^=84Am0y3+ zJc}-@;rdL(c3mdyAKxi+StV;DizZKk%?wDaXNZ?@8_FdWpj6s_cQM|iG=ta2Gx<}w z19};|M-oG2x9}unas3g^8|LjSfR4VwEKR8d77o$WTFk8KB|C+3i741(tB zThb;r2{d>8RkTLRuB@@HLD_pJWkXEfnXR`c!q!CxRoXcZeRAVbiCaW#z-s*I$QmW? z_HQ0ySOIrIDS1JE=7Y9mtq+EeHiR~Eq<)k8+-TUQvY6B#TZD4-|I9PHu5OW}xM+B# zn4&fo`}r(SonUts`LX&sfjt&Fq`m{66!)as_1k6w)7jGE92l{i(_;17JSO=wsG3TD zo/dev/null; then + # OpenSSL < v1.1.0 + "${openssl_path}" "list-$arg" + else + # OpenSSL >= v1.1.0 + "${openssl_path}" "list" "-$arg" + fi +} + +_is_contained_str(){ + __doc__=' + Args: + arg : the query to check if it is contained in the values + values : a string of space separated values + + Example: + # Demo using raw call + (_is_contained_str "foo" "foo bar baz" && echo "contained") || echo "missing" + (_is_contained_str "bar" "foo bar baz" && echo "contained") || echo "missing" + (_is_contained_str "baz" "foo bar baz" && echo "contained") || echo "missing" + (_is_contained_str "biz" "foo bar baz" && echo "contained") || echo "missing" + # Demo using variables + arg="bar" + values="foo bar baz" + (_is_contained_str "$arg" "$values" && echo "contained") || echo "missing" + ' + arg=$1 + values=$2 + echo "$values" | tr -s ' ' '\n' | grep -Fx "$arg" &>/dev/null +} + +_load_versioned_config_var(){ + local key=$1 + # Test for blocked variables that should not go into a plaintext config file + if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then + warn "Cannot use ${key} in versioned the transcrypt config" + return 1 + fi + git config --file "${VERSIONED_TC_CONFIG}" --get "${key}" || true +} + +_set_versioned_config_var(){ + local key=$1 + local val=$2 + # Test for blocked variables that should not go into a plaintext config file + if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then + warn "Cannot use ${key} in versioned the transcrypt config" + return 1 + fi + mkdir -p "${VERSIONED_TC_DIR}" + git config --file "${VERSIONED_TC_CONFIG}" "${key}" "${val}" +} + +_load_config_var(){ + # First try loading from the local checkout-independent .git/config file + # If that doesn't work, then look in the .transcrypt/config file + # (which is expected to be stored in plaintext and checked into the repo) + # Certain values will be blocked from being placed here (like the password) + local key=$1 + git config --get --local "${key}" + if [[ "$?" != "0" ]]; then + _load_versioned_config_var "${key}" + fi +} + +# shellcheck disable=SC2155 +_load_transcrypt_config_vars(){ + # Populate bash vars with our config + cipher=$(git config --get --local transcrypt.cipher) + digest=$(git config --get --local transcrypt.digest) + use_pbkdf2=$(git config --get --local transcrypt.use-pbkdf2) + salt_method=$(git config --get --local transcrypt.salt-method) + + password=$(git config --get --local transcrypt.password) + openssl_path=$(git config --get --local transcrypt.openssl-path) + + # the current git repository's top-level directory + readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) + + # This is the place where transcrypt can store state that will be + # "versioned" (i.e. checked into the repo) + readonly VERSIONED_TC_DIR="${REPO}/.transcrypt" + readonly VERSIONED_TC_CONFIG="${VERSIONED_TC_DIR}/config" +} + + +_load_vars_for_encryption(){ + # Helper to populate variables needed to call openssl encryption + _load_transcrypt_config_vars + + if [[ "$use_pbkdf2" == "1" ]]; then + pbkdf2_args=('-pbkdf2') + else + pbkdf2_args=() + fi + + if [[ "$salt_method" == "password" ]]; then + extra_salt=$password + elif [[ "$salt_method" == "configured" ]]; then + extra_salt=$(_load_versioned_config_var "transcrypt.extra-salt") + else + die "unknown salt method" + fi + + if [[ "$extra_salt" == "" ]]; then + die "Extra salt is not set" + fi +} + + + # establish repository metadata and directory handling # shellcheck disable=SC2155 gather_repo_metadata() { @@ -90,6 +300,11 @@ gather_repo_metadata() { else readonly GIT_ATTRIBUTES="${REPO}/.gitattributes" fi + + # This is the place where transcrypt can store state that will be + # "versioned" (i.e. checked into the repo) + readonly VERSIONED_TC_DIR="${REPO}/.transcrypt" + readonly VERSIONED_TC_CONFIG="${VERSIONED_TC_DIR}/config" } # print a message to stderr @@ -116,9 +331,8 @@ die() { # 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 +# (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() { # The clean script encrypts files before git sends them to the remote. @@ -147,14 +361,12 @@ git_clean() { # The file is already encrypted, so just pass it back 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) + _load_vars_for_encryption #salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16) #ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -e -a -S "$salt" -in "$tempfile" # NOTE: salt must be 16 bytes, its openssl standard - salt=$("$openssl_path" dgst -hmac "${filename}:${password}" -sha512 "$filename" | tr -d '\r\n' | tail -c 16) - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md SHA512 -pass env:ENC_PASS -pbkdf2 -e -a -S "$salt" -in "$tempfile" + salt=$("$openssl_path" dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16) + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" fi } @@ -163,11 +375,9 @@ git_smudge() { # the file contents are passed via stdin 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) + _load_transcrypt_config_vars #tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -d -a 2>/dev/null || cat "$tempfile" - tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md SHA512 -pass env:ENC_PASS -pbkdf2 -d -a 2>/dev/null || cat "$tempfile" + tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a 2>/dev/null || cat "$tempfile" } git_textconv() { @@ -179,11 +389,9 @@ git_textconv() { 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) + _load_transcrypt_config_vars #ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2>/dev/null || cat "$filename" - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md SHA512 -pass env:ENC_PASS -pbkdf2 -d -a -in "$filename" 2>/dev/null || cat "$filename" + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a -in "$filename" 2>/dev/null || cat "$filename" } # shellcheck disable=SC2005,SC2002,SC2181 @@ -245,6 +453,7 @@ git_pre_commit() { : # Do nothing # The first bytes of an encrypted file must be "Salted" in Base64 elif [[ $firstbytes != "U2FsdGVk" ]]; then + echo "firstbytes = $firstbytes" 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 @@ -333,48 +542,52 @@ run_safety_checks() { # unset the cipher variable if it is not supported by openssl validate_cipher() { - local list_cipher_commands - if "${openssl_path}" list-cipher-commands &>/dev/null; then - # OpenSSL < v1.1.0 - list_cipher_commands="${openssl_path} list-cipher-commands" - else - # OpenSSL >= v1.1.0 - list_cipher_commands="${openssl_path} list -cipher-commands" - fi + local valid_ciphers + valid_ciphers=$(_openssl_list cipher-commands) + _validate_variable_str "cipher" "$valid_ciphers" +} - local supported - 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 | column -c 80 - printf '\n' - cipher='' - else - # shellcheck disable=SC2016 - die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands" - fi - fi +validate_digest() { + local valid_digests + valid_digests=$(_openssl_list digest-commands) + _validate_variable_str "digest" "$valid_digests" } +validate_use_pbkdf2() { + _validate_variable_str "use_pbkdf2" "0 1" +} + + +validate_salt_method(){ + _validate_variable_str "salt_method" "$VALID_SALT_METHODS" +} + +# ensure we have a digest to hash the salted password +get_digest() { + local prompt + prompt=$(printf 'Encrypt using which digest? [%s] ' "$DEFAULT_DIGEST") + _get_user_input digest "$DEFAULT_DIGEST" "validate_digest" "$prompt" +} + + # 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 + local prompt + prompt=$(printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER") + _get_user_input cipher "$DEFAULT_CIPHER" "validate_cipher" "$prompt" +} - # 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 +get_use_pbkdf2() { + local prompt + prompt=$(printf 'Use pbkdf2? [%s] ' "$DEFAULT_USE_PBKDF2") + _get_user_input use_pbkdf2 "$DEFAULT_USE_PBKDF2" "validate_use_pbkdf2" "$prompt" +} + + +get_salt_method() { + local prompt + prompt=$(printf 'Compute salt using which method? [%s] ' "$DEFAULT_SALT_METHOD") + _get_user_input salt_method "$DEFAULT_SALT_METHOD" "validate_salt_method" "$prompt" } # ensure we have a password to encrypt with @@ -402,17 +615,14 @@ get_password() { 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" + _display_runtime_configuration printf 'Does this look correct? [Y/n] ' read -r -n 1 -s answer @@ -429,13 +639,7 @@ confirm_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" + _display_runtime_configuration 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] ' @@ -472,8 +676,13 @@ save_helper_scripts() { local current_transcrypt current_transcrypt=$(realpath "$0" 2>/dev/null) - cp "$current_transcrypt" "${CRYPT_DIR}/transcrypt" - + EDITABLE_INSTALL=1 + if [[ "$EDITABLE_INSTALL" == "1" ]]; then + # Editable mode is for debugging + ln -fs "$current_transcrypt" "${CRYPT_DIR}/transcrypt" + else + cp "$current_transcrypt" "${CRYPT_DIR}/transcrypt" + fi # make scripts executable for script in {transcrypt,}; do chmod 0755 "${CRYPT_DIR}/${script}" @@ -513,9 +722,34 @@ save_configuration() { # write the encryption info git config transcrypt.version "$VERSION" + + git config transcrypt.openssl-path "$openssl_path" + git config transcrypt.cipher "$cipher" + git config transcrypt.digest "$digest" + git config transcrypt.use-pbkdf2 "$use_pbkdf2" + git config transcrypt.salt-method "$salt_method" + git config transcrypt.password "$password" - git config transcrypt.openssl-path "$openssl_path" + + use_versioned_config=1 + if [[ "$use_versioned_config" == "1" ]]; then + _set_versioned_config_var "transcrypt.version" "$VERSION" + _set_versioned_config_var "transcrypt.cipher" "$cipher" + _set_versioned_config_var "transcrypt.digest" "$digest" + _set_versioned_config_var "transcrypt.use-pbkdf2" "$use_pbkdf2" + _set_versioned_config_var "transcrypt.salt-method" "$salt_method" + #_set_versioned_config_var "transcrypt.extra-salt" "$use_pbkdf2" + fi + if [[ "$salt_method" == "configured" ]]; then + git config transcrypt.extra-salt "$extra_salt" + _set_versioned_config_var "transcrypt.extra-salt" "$extra_salt" + if ! git ls-files --error-unmatch "$VERSIONED_TC_CONFIG"; then + echo "When using a configured salt method, you must commit " + echo "the ${VERSIONED_TC_CONFIG} file to the repo in order " + echo "to get consistent encryption across machines" + fi + fi # write the filter settings. Sorry for the horrific quote escaping below... # shellcheck disable=SC2016 @@ -536,23 +770,63 @@ save_configuration() { 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 }'" } +# Show the config of the current runtime +_display_runtime_configuration(){ + 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 ' DIGEST: %s\n' "$digest" + printf ' USE_PBKDF2: %s\n' "$use_pbkdf2" + printf ' SALT_METHOD: %s\n' "$salt_method" + if [[ "$salt_method" == "configured" ]]; then + printf ' EXTRA_SALT: %s\n' "$extra_salt" + fi + printf ' CIPHER: %s\n' "$cipher" + printf ' PASSWORD: %s\n\n' "$password" +} + # display the current configuration settings display_configuration() { local current_cipher current_cipher=$(git config --get --local transcrypt.cipher) + + local current_digest + current_digest=$(git config --get --local transcrypt.digest) + + local current_salt_method + current_salt_method=$(git config --get --local transcrypt.salt-method) + + local current_use_pbkdf2 + current_use_pbkdf2=$(git config --get --local transcrypt.use-pbkdf2) + local current_password current_password=$(git config --get --local transcrypt.password) local escaped_password=${current_password//\'/\'\\\'\'} + local current_extra_salt + if [[ "$current_salt_method" == "configured" ]]; then + current_extra_salt=$(_load_versioned_config_var "transcrypt.extra-salt") + echo "current_extra_salt = $current_extra_salt" + fi + 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 ' DIGEST: %s\n' "$current_digest" + printf ' USE_PBKDF2: %s\n' "$current_use_pbkdf2" + printf ' SALT_METHOD: %s\n' "$current_salt_method" + if [[ "$current_salt_method" == "configured" ]]; then + printf ' EXTRA_SALT: %s\n' "$current_extra_salt" + fi + 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" + printf " transcrypt -c %s -p '%s' --md '%s' --use_pbkdf2 '%s' --sm '%s'\n" \ + "$current_cipher" "$escaped_password" "$current_digest" "$current_use_pbkdf2" "$current_salt_method" } # remove transcrypt-related settings from the repository's git config @@ -900,6 +1174,18 @@ help() { the password to derive the key from; defaults to 30 random base64 characters + -md, --digest=DIGEST + the digest used to hash the salted password; + defaults to md5 + + -pbkdf2, --use_pbkdf2=USE_PBKDF2 + Use the pbkdf2 openssl encryption feature; + defaults to 0 + + -sm, --salt_method=SALT_METHOD + Method used to compute deterministic salt; can be password or configured + defaults to password + --set-openssl-path=PATH_TO_OPENSSL use OpenSSL at this path; defaults to 'openssl' in \$PATH @@ -1006,6 +1292,10 @@ uninstall='' upgrade='' openssl_path='openssl' +use_pbkdf2='' +digest='' +salt_method='' + # used to bypass certain safety checks requires_existing_config='' requires_clean_repo='true' @@ -1046,6 +1336,30 @@ while [[ "${1:-}" != '' ]]; do --cipher=*) cipher=${1#*=} ;; + -md | --digest) + digest=$2 + shift + ;; + --digest=*) + digest=${1#*=} + ;; + -pbkdf2) + use_pbkdf2=1 + ;; + --use-pbkdf2) + use_pbkdf2=${2} + shift + ;; + --use-pbkdf2=*) + use_pbkdf2=${1#*=} + ;; + -sm | --salt-method) + salt_method=$2 + shift + ;; + --salt-method=*) + salt_method=${1#*=} + ;; -p | --password) password=$2 shift @@ -1183,8 +1497,20 @@ fi # perform function calls to configure transcrypt get_cipher +get_digest +get_use_pbkdf2 +get_salt_method get_password +if [[ "$salt_method" == "configured" ]]; then + extra_salt=$(_load_versioned_config_var "transcrypt.extra-salt") + # If we have not configured the extra salt (or we need to rekey), + # then generate new random salt + if [[ "$extra_salt" == "" ]] || [[ $rekey ]]; then + extra_salt=$(openssl rand -hex 32) + fi +fi + if [[ $rekey ]] && [[ $interactive ]]; then confirm_rekey elif [[ $interactive ]]; then From a7bb0fabdc2a8bb2c898dcc2b94d1c176bddb131 Mon Sep 17 00:00:00 2001 From: joncrall Date: Thu, 5 May 2022 22:29:56 -0400 Subject: [PATCH 03/15] Working on python API and tests Tests verify all param combinations wip --- example/end_to_end_example.sh | 1 - tests/test_transcrypt.py | 263 +++++++++++++++++++++++++++++++++ transcrypt | 265 ++++++++++++++++------------------ 3 files changed, 388 insertions(+), 141 deletions(-) create mode 100644 tests/test_transcrypt.py diff --git a/example/end_to_end_example.sh b/example/end_to_end_example.sh index dab84fa..67eb7ca 100644 --- a/example/end_to_end_example.sh +++ b/example/end_to_end_example.sh @@ -8,7 +8,6 @@ mkdir -p "$TMP_DIR" rm -rf "$TMP_DIR" - # Make a git repo and add some public content DEMO_REPO=$TMP_DIR/repo mkdir -p "$DEMO_REPO" diff --git a/tests/test_transcrypt.py b/tests/test_transcrypt.py new file mode 100644 index 0000000..6467865 --- /dev/null +++ b/tests/test_transcrypt.py @@ -0,0 +1,263 @@ +""" +Requirements: + pip install gpg_lite + pip install ubelt +""" +import ubelt as ub +import os + +SALTED = 'U2FsdGV' + + +class TranscryptAPI: + default_config = { + 'cipher': 'aes-256-cbc', + 'password': 'correct horse battery staple', + 'digest': 'md5', + 'use_pbkdf2': '0', + 'salt_method': 'password', + 'config_salt': '', + } + + def __init__(self, dpath, config=None, verbose=2, transcript_exe=None): + self.dpath = dpath + self.verbose = verbose + self.transcript_exe = ub.Path(ub.find_exe('transcrypt')) + self.env = {} + self.config = self.default_config.copy() + if config: + self.config.update(config) + + def cmd(self, command, shell=False): + return ub.cmd(command, cwd=self.dpath, verbose=self.verbose, + env=self.env, shell=shell) + + def login(self): + command = ( + "{transcript_exe} -c '{cipher}' -p '{password}' " + "-md '{digest}' --use-pbkdf2 '{use_pbkdf2}' " + "-sm '{salt_method}' " + "-cs '{config_salt}' " + "-y" + ).format(transcript_exe=self.transcript_exe, **self.config) + self.cmd(command) + + def logout(self): + self.cmd(f'{self.transcript_exe} -f -y') + + def display(self): + self.cmd(f'{self.transcript_exe} -d') + + def export_gpg(self, recipient): + self.cmd(f'{self.transcript_exe} --export-gpg "{recipient}"') + self.crypt_dpath = self.cmd('git config --local transcrypt.crypt-dir')['out'] or self.dpath / '.git/crypt' + asc_fpath = (self.crypt_dpath / (recipient + '.asc')) + return asc_fpath + + def import_gpg(self, asc_fpath): + command = f"{self.transcript_exe} --import-gpg '{asc_fpath}' -y" + self.cmd(command) + + def show_raw(self, fpath): + return self.cmd(f'{self.transcript_exe} -s {fpath}')['out'] + + def _manual_hack_info(self): + """ + Info on how to get an env to run a failing command manually + """ + for k, v in self.env.items(): + print(f'export {k}={v}') + print(f'cd {self.dpath}') + + +class TestEnvironment: + + def __init__(self, dpath=None, config=None, verbose=2): + if dpath is None: + # import tempfile + # self._tmpdir = tempfile.TemporaryDirectory() + # dpath = self._tmpdir.name + dpath = ub.Path.appdir('transcrypt/tests/test_env') + self.dpath = ub.Path(dpath) + self.gpg_store = None + self.repo_dpath = None + self.verbose = verbose + self.tc = None + self.config = config + + def setup(self): + self._setup_gpg() + self._setup_git() + self._setup_transcrypt() + return self + + def _setup_gpg(self): + import gpg_lite + self.gpg_home = (self.dpath / 'gpg').ensuredir() + self.gpg_store = gpg_lite.GPGStore( + gnupg_home_dir=self.gpg_home + ) + self.gpg_fpr = self.gpg_store.gen_key( + full_name='Emmy Noether', + email='emmy.noether@uni-goettingen.de', + passphrase=None, + key_type='eddsa', + subkey_type='ecdh', + key_curve='Ed25519', + subkey_curve='Curve25519' + ) + # Fix GNUPG permissions + (self.gpg_home / 'private-keys-v1.d').ensuredir() + # 600 for files and 700 for directories + ub.cmd('find ' + str(self.gpg_home) + r' -type f -exec chmod 600 {} \;', shell=True, verbose=self.verbose, cwd=self.gpg_home) + ub.cmd('find ' + str(self.gpg_home) + r' -type d -exec chmod 700 {} \;', shell=True, verbose=self.verbose, cwd=self.gpg_home) + + def _setup_git(self): + import git + # Make a git repo and add some public content + repo_name = 'demo-repo' + self.repo_dpath = (self.dpath / repo_name).ensuredir() + # self.repo_dpath.delete().ensuredir() + self.repo_dpath.ensuredir() + + for content in self.repo_dpath.iterdir(): + content.delete() + + self.git = git.Git(self.repo_dpath) + self.git.init() + readme_fpath = (self.repo_dpath / 'README.md') + readme_fpath.write_text('content') + self.git.add(readme_fpath) + + # Create safe directory that we will encrypt + gitattr_fpath = self.repo_dpath / '.gitattributes' + gitattr_fpath.write_text(ub.codeblock( + ''' + safe/* filter=crypt diff=crypt merge=crypt + ''')) + self.git.add(gitattr_fpath) + self.git.commit('-am Add initial contents') + self.safe_dpath = (self.repo_dpath / 'safe').ensuredir() + self.secret_fpath = self.safe_dpath / 'secret.txt' + + def _setup_transcrypt(self): + self.tc = TranscryptAPI(self.repo_dpath, self.config, + verbose=self.verbose) + err = self.tc.cmd(f'{self.tc.transcript_exe} -d')['err'].strip() + if err != 'transcrypt: the current repository is not configured': + raise AssertionError(f"Got {err}") + self.tc.login() + self.secret_fpath.write_text('secret content') + self.git.add(self.secret_fpath) + self.git.commit('-am add secret') + self.tc.display() + if self.gpg_home is not None: + self.tc.env['GNUPGHOME'] = str(self.gpg_home) + + def test_round_trip(self): + ciphertext = self.tc.show_raw(self.secret_fpath) + plaintext = self.secret_fpath.read_text() + assert ciphertext.startswith(SALTED) + assert plaintext.startswith('secret content') + assert not plaintext.startswith(SALTED) + + self.tc.logout() + logged_out_text = self.secret_fpath.read_text() + assert logged_out_text == ciphertext + + self.tc.login() + logged_in_text = self.secret_fpath.read_text() + + assert logged_out_text == ciphertext + assert logged_in_text == plaintext + + def test_export_gpg(self): + self.tc.display() + asc_fpath = self.tc.export_gpg(self.gpg_fpr) + + info = self.tc.cmd(f'gpg --batch --quiet --decrypt "{asc_fpath}"') + content = info['out'] + + got_config = dict([p.split('=', 1) for p in content.split('\n') if p]) + config = self.tc.config.copy() + is_ok = got_config == config + if not is_ok: + if config['salt_method'] == 'configured': + if config['config_salt'] == '': + config.pop('config_salt') + got_config.pop('config_salt') + is_ok = got_config == config + else: + config.pop('config_salt') + got_config.pop('config_salt') + is_ok = got_config == config + + if not is_ok: + print(f'got_config={got_config}') + print(f'config={config}') + raise AssertionError + + # content = io.StringIO() + # with open(asc_fpath, 'r') as file: + # ciphertext = file.read() + # self.gpg_store.decrypt(ciphertext, content) + + assert asc_fpath.exists() + self.tc.logout() + self.tc.import_gpg(asc_fpath) + + plaintext = self.secret_fpath.read_text() + assert plaintext.startswith('secret content') + + def test_rekey(self): + # TODO + pass + + +def run_tests(): + """ + CommandLine: + xdoctest -m /home/joncrall/code/transcrypt/tests/test_transcrypt.py run_tests + + Example: + >>> import sys, ubelt + >>> sys.path.append(ubelt.expandpath('~/code/transcrypt/tests')) + >>> from test_transcrypt import * # NOQA + >>> self = TestEnvironment() + >>> self.setup() + >>> self.tc._manual_hack_info() + >>> self.test_round_trip() + >>> self.test_export_gpg() + + self = TestEnvironment(config={'use_pbkdf2': 1}) + self.setup() + self.test_round_trip() + self.test_export_gpg() + + self = TestEnvironment(config={'use_pbkdf2': 1}) + """ + + # Test that transcrypt works under a variety of config conditions + basis = { + 'cipher': ['aes-256-cbc'], + 'password': ['correct horse battery staple'], + 'digest': ['md5', 'sha256'], + 'use_pbkdf2': ['0', '1'], + 'salt_method': ['password', 'configured'], + 'config_salt': ['', 'mylittlecustomsalt'], + } + + for params in ub.named_product(basis): + config = params.copy() + self = TestEnvironment(config=config) + self.setup() + self.test_round_trip() + self.test_export_gpg() + + +if __name__ == '__main__': + """ + CommandLine: + python ~/code/transcrypt/tests/test_transcrypt.py + """ + run_tests() diff --git a/transcrypt b/transcrypt index 7ad2412..3a416b2 100755 --- a/transcrypt +++ b/transcrypt @@ -1,6 +1,11 @@ #!/usr/bin/env bash set -euo pipefail +# TODO: +# Remove "salt_method" +# Just have a setting where the password is allowed to be in the salt and a +# setting where the user does have a configured salt? + # # transcrypt - https://github.com/elasticdog/transcrypt # @@ -31,6 +36,9 @@ readonly VALID_SALT_METHODS="password configured" # configuration readonly VERSIONED_CONFIG_BLOCKLIST="transcrypt.password transcrypt.openssl-path" +# Set to 1 to enable a development editable installation +readonly EDITABLE_INSTALL=1 + ##### FUNCTIONS # print a canonicalized absolute pathname @@ -61,30 +69,6 @@ realpath() { fi } -joinby(){ - __doc__=' - Join an array into a string - - Args: - SEP: the separator - *ARR: elements of the strings to join - - Usage: - source $HOME/local/init/utils.sh - ARR=("foo" "bar" "baz") - RESULT=$(joinby / "${ARR[@]}") - echo "RESULT = $RESULT" - RESULT = foo/bar/baz - - References: - https://stackoverflow.com/questions/1527049/how-can-i-join-elements-of-an-array-in-bash - ' - local d=${1-} f=${2-} - if shift 2; then - printf %s "$f" "${@/#/$d}" - fi -} - # sets a bash global variable by name _set_global(){ key=$1 @@ -93,15 +77,9 @@ _set_global(){ } +# Checks if the target variable is in the set of valid values. If it is not, it +# unsets the target variable, then if not in interactive mode it calls die. _validate_variable_str(){ - #__doc__=' - #Checks if the target variable is in the set of valid values. - #If it is not, it unsets the target variable, then if not in interactive - #mode it calls die. - #Args: - # varname: name of variable to validate - # valid_values: space separated string of valid values - #' local varname=$1 local valid_values=$2 local varval=${!varname} @@ -116,13 +94,8 @@ _validate_variable_str(){ fi } +# Helper to prompt the user, store a response, and validate the result _get_user_input() { - # Helper to prompt the user, store a response, and validate the result - # Args: - # varname : name of the bash variable to populate - # default : the default value to use if the user provides no answer - # validate_fn : the name of a function that validates the user input - # prompt : string to present to the user local varname=$1 local default=$2 local validate_fn=$3 @@ -146,9 +119,8 @@ _get_user_input() { done } - +# compatible openssl list command _openssl_list(){ - # Args: the openssl commands to list arg=$1 if "${openssl_path} list-$arg" &>/dev/null; then # OpenSSL < v1.1.0 @@ -159,29 +131,25 @@ _openssl_list(){ fi } +# Check if the first arg is contained in the space separated second arg _is_contained_str(){ - __doc__=' - Args: - arg : the query to check if it is contained in the values - values : a string of space separated values - - Example: - # Demo using raw call - (_is_contained_str "foo" "foo bar baz" && echo "contained") || echo "missing" - (_is_contained_str "bar" "foo bar baz" && echo "contained") || echo "missing" - (_is_contained_str "baz" "foo bar baz" && echo "contained") || echo "missing" - (_is_contained_str "biz" "foo bar baz" && echo "contained") || echo "missing" - # Demo using variables - arg="bar" - values="foo bar baz" - (_is_contained_str "$arg" "$values" && echo "contained") || echo "missing" - ' arg=$1 values=$2 echo "$values" | tr -s ' ' '\n' | grep -Fx "$arg" &>/dev/null } + +# Load a config var from the versioned config +# shellcheck disable=SC2155 _load_versioned_config_var(){ + # the current git repository's top-level directory + if [ -z "${VERSIONED_TC_CONFIG+x}" ]; then + readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) + # This is the place where transcrypt can store state that will be + # "versioned" (i.e. checked into the repo) + readonly VERSIONED_TC_DIR="${REPO}/.transcrypt" + readonly VERSIONED_TC_CONFIG="${VERSIONED_TC_DIR}/config" + fi local key=$1 # Test for blocked variables that should not go into a plaintext config file if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then @@ -191,6 +159,7 @@ _load_versioned_config_var(){ git config --file "${VERSIONED_TC_CONFIG}" --get "${key}" || true } +# Write a config var to the versioned config _set_versioned_config_var(){ local key=$1 local val=$2 @@ -203,6 +172,7 @@ _set_versioned_config_var(){ git config --file "${VERSIONED_TC_CONFIG}" "${key}" "${val}" } +# _load_config_var(){ # First try loading from the local checkout-independent .git/config file # If that doesn't work, then look in the .transcrypt/config file @@ -226,13 +196,11 @@ _load_transcrypt_config_vars(){ password=$(git config --get --local transcrypt.password) openssl_path=$(git config --get --local transcrypt.openssl-path) - # the current git repository's top-level directory - readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) - - # This is the place where transcrypt can store state that will be - # "versioned" (i.e. checked into the repo) - readonly VERSIONED_TC_DIR="${REPO}/.transcrypt" - readonly VERSIONED_TC_CONFIG="${VERSIONED_TC_DIR}/config" + if [[ "$salt_method" == "configured" ]]; then + config_salt=$(_load_versioned_config_var "transcrypt.config-salt") + else + config_salt="" + fi } @@ -249,7 +217,7 @@ _load_vars_for_encryption(){ if [[ "$salt_method" == "password" ]]; then extra_salt=$password elif [[ "$salt_method" == "configured" ]]; then - extra_salt=$(_load_versioned_config_var "transcrypt.extra-salt") + extra_salt=$config_salt else die "unknown salt method" fi @@ -260,7 +228,6 @@ _load_vars_for_encryption(){ } - # establish repository metadata and directory handling # shellcheck disable=SC2155 gather_repo_metadata() { @@ -303,8 +270,10 @@ gather_repo_metadata() { # This is the place where transcrypt can store state that will be # "versioned" (i.e. checked into the repo) - readonly VERSIONED_TC_DIR="${REPO}/.transcrypt" - readonly VERSIONED_TC_CONFIG="${VERSIONED_TC_DIR}/config" + readonly RELATIVE_VERSIONED_TC_DIR=".transcrypt" + readonly RELATIVE_VERSIONED_TC_CONFIG="${RELATIVE_VERSIONED_TC_DIR}/config" + readonly VERSIONED_TC_DIR="${REPO}/${RELATIVE_VERSIONED_TC_DIR}" + readonly VERSIONED_TC_CONFIG="${REPO}/${RELATIVE_VERSIONED_TC_CONFIG}" } # print a message to stderr @@ -362,8 +331,6 @@ git_clean() { cat "$tempfile" else _load_vars_for_encryption - #salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16) - #ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -e -a -S "$salt" -in "$tempfile" # NOTE: salt must be 16 bytes, its openssl standard salt=$("$openssl_path" dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16) ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" @@ -375,8 +342,8 @@ git_smudge() { # the file contents are passed via stdin tempfile=$(mktemp 2>/dev/null || mktemp -t tmp) trap 'rm -f "$tempfile"' EXIT - _load_transcrypt_config_vars - #tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -d -a 2>/dev/null || cat "$tempfile" + #_load_transcrypt_config_vars + _load_vars_for_encryption tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a 2>/dev/null || cat "$tempfile" } @@ -390,7 +357,6 @@ git_textconv() { return fi _load_transcrypt_config_vars - #ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md MD5 -pass env:ENC_PASS -d -a -in "$filename" 2>/dev/null || cat "$filename" ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a -in "$filename" 2>/dev/null || cat "$filename" } @@ -616,12 +582,12 @@ get_password() { } - - # confirm the transcrypt configuration confirm_configuration() { local answer= + _display_git_configuration + printf 'The following configuration will be saved:\n\n' _display_runtime_configuration printf 'Does this look correct? [Y/n] ' read -r -n 1 -s answer @@ -639,6 +605,8 @@ confirm_configuration() { confirm_rekey() { local answer= + _display_git_configuration + printf 'The following configuration will be saved:\n\n' _display_runtime_configuration 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' @@ -676,7 +644,6 @@ save_helper_scripts() { local current_transcrypt current_transcrypt=$(realpath "$0" 2>/dev/null) - EDITABLE_INSTALL=1 if [[ "$EDITABLE_INSTALL" == "1" ]]; then # Editable mode is for debugging ln -fs "$current_transcrypt" "${CRYPT_DIR}/transcrypt" @@ -732,22 +699,28 @@ save_configuration() { git config transcrypt.password "$password" - use_versioned_config=1 - if [[ "$use_versioned_config" == "1" ]]; then - _set_versioned_config_var "transcrypt.version" "$VERSION" - _set_versioned_config_var "transcrypt.cipher" "$cipher" - _set_versioned_config_var "transcrypt.digest" "$digest" - _set_versioned_config_var "transcrypt.use-pbkdf2" "$use_pbkdf2" - _set_versioned_config_var "transcrypt.salt-method" "$salt_method" - #_set_versioned_config_var "transcrypt.extra-salt" "$use_pbkdf2" - fi + # TODO: We may want to allow repo settings to be stored here as well. + #if [[ "$use_versioned_config" == "1" ]]; then + # _set_versioned_config_var "transcrypt.version" "$VERSION" + # _set_versioned_config_var "transcrypt.cipher" "$cipher" + # _set_versioned_config_var "transcrypt.digest" "$digest" + # _set_versioned_config_var "transcrypt.use-pbkdf2" "$use_pbkdf2" + # _set_versioned_config_var "transcrypt.salt-method" "$salt_method" + #fi if [[ "$salt_method" == "configured" ]]; then - git config transcrypt.extra-salt "$extra_salt" - _set_versioned_config_var "transcrypt.extra-salt" "$extra_salt" - if ! git ls-files --error-unmatch "$VERSIONED_TC_CONFIG"; then - echo "When using a configured salt method, you must commit " - echo "the ${VERSIONED_TC_CONFIG} file to the repo in order " - echo "to get consistent encryption across machines" + # TODO: we may want to also write the config-salt variable to the local config + # The user might not care about cross machine transparency + git config transcrypt.config-salt "$config_salt" + _set_versioned_config_var "transcrypt.config-salt" "$config_salt" + if ! git ls-files --error-unmatch "$RELATIVE_VERSIONED_TC_CONFIG" > /dev/null 2>&1; then + git add "${RELATIVE_VERSIONED_TC_CONFIG}" + printf "*** The contents of %s were configured. ***\n" "${RELATIVE_VERSIONED_TC_CONFIG}" + printf '*** COMMIT THIS CHANGE RIGHT AWAY! ***\n\n' + fi + if ! git diff --exit-code "$RELATIVE_VERSIONED_TC_CONFIG" > /dev/null 2>&1; then + git add "${RELATIVE_VERSIONED_TC_CONFIG}" + printf "*** The contents of %s were updated. ***\n" "${RELATIVE_VERSIONED_TC_CONFIG}" + printf '*** COMMIT THIS CHANGE RIGHT AWAY! ***\n\n' fi fi @@ -770,18 +743,21 @@ save_configuration() { 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 }'" } -# Show the config of the current runtime -_display_runtime_configuration(){ + +_display_git_configuration(){ 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' +} + +# Show the config of the current runtime +_display_runtime_configuration(){ printf ' DIGEST: %s\n' "$digest" printf ' USE_PBKDF2: %s\n' "$use_pbkdf2" printf ' SALT_METHOD: %s\n' "$salt_method" if [[ "$salt_method" == "configured" ]]; then - printf ' EXTRA_SALT: %s\n' "$extra_salt" + printf ' CONFIG_SALT: %s\n' "$config_salt" fi printf ' CIPHER: %s\n' "$cipher" printf ' PASSWORD: %s\n\n' "$password" @@ -789,44 +765,30 @@ _display_runtime_configuration(){ # display the current configuration settings display_configuration() { - local current_cipher - current_cipher=$(git config --get --local transcrypt.cipher) - - local current_digest - current_digest=$(git config --get --local transcrypt.digest) - - local current_salt_method - current_salt_method=$(git config --get --local transcrypt.salt-method) - - local current_use_pbkdf2 - current_use_pbkdf2=$(git config --get --local transcrypt.use-pbkdf2) - - local current_password - current_password=$(git config --get --local transcrypt.password) - local escaped_password=${current_password//\'/\'\\\'\'} - - local current_extra_salt - if [[ "$current_salt_method" == "configured" ]]; then - current_extra_salt=$(_load_versioned_config_var "transcrypt.extra-salt") - echo "current_extra_salt = $current_extra_salt" - fi - + _load_transcrypt_config_vars + local escaped_password=${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 ' DIGEST: %s\n' "$current_digest" - printf ' USE_PBKDF2: %s\n' "$current_use_pbkdf2" - printf ' SALT_METHOD: %s\n' "$current_salt_method" - if [[ "$current_salt_method" == "configured" ]]; then - printf ' EXTRA_SALT: %s\n' "$current_extra_salt" - fi - printf ' CIPHER: %s\n' "$current_cipher" - printf ' PASSWORD: %s\n\n' "$current_password" + _display_git_configuration + _display_runtime_configuration printf 'Copy and paste the following command to initialize a cloned repository:\n\n' - printf " transcrypt -c %s -p '%s' --md '%s' --use_pbkdf2 '%s' --sm '%s'\n" \ - "$current_cipher" "$escaped_password" "$current_digest" "$current_use_pbkdf2" "$current_salt_method" + printf " transcrypt -c '%s' -p '%s' -md '%s' --use-pbkdf2 '%s' -sm '%s' -cs '%s'\n" \ + "$cipher" "$escaped_password" "$digest" "$use_pbkdf2" "$salt_method" "$config_salt" + + #[[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO" + #printf ' GIT_DIR: %s\n' "$GIT_DIR" + #printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES" + #printf ' DIGEST: %s\n' "$current_digest" + #printf ' USE_PBKDF2: %s\n' "$current_use_pbkdf2" + #printf ' SALT_METHOD: %s\n' "$current_salt_method" + #if [[ "$current_salt_method" == "configured" ]]; then + # printf ' CONFIG_SALT: %s\n' "$current_config_salt" + #fi + #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' -md '%s' --use_pbkdf2 '%s' -sm '%s'\n" \ + # "$current_cipher" "$escaped_password" "$current_digest" "$current_use_pbkdf2" "$current_salt_method" } # remove transcrypt-related settings from the repository's git config @@ -1096,14 +1058,19 @@ export_gpg() { 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) + _load_transcrypt_config_vars + + #local current_cipher + #current_cipher=$(git config --get --local transcrypt.cipher) + #local current_password + #current_password=$(git config --get --local transcrypt.password) 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 >"${CRYPT_DIR}/${gpg_recipient}.asc" + #printf 'password=%s\ncipher=%s\n' "$current_password" "$current_cipher" | $gpg_encrypt_cmd >"${CRYPT_DIR}/${gpg_recipient}.asc" + printf 'password=%s\ncipher=%s\ndigest=%s\nuse_pbkdf2=%s\nsalt_method=%s\nconfig_salt=%s\n\n' \ + "$password" "$cipher" "$digest" "$use_pbkdf2" "$salt_method" "$config_salt" | \ + $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" } @@ -1136,6 +1103,10 @@ import_gpg() { cipher=$(printf '%s' "$configuration" | grep '^cipher' | cut -d'=' -f 2-) password=$(printf '%s' "$configuration" | grep '^password' | cut -d'=' -f 2-) + digest=$(printf '%s' "$configuration" | grep '^digest' | cut -d'=' -f 2-) + use_pbkdf2=$(printf '%s' "$configuration" | grep '^use_pbkdf2' | cut -d'=' -f 2-) + salt_method=$(printf '%s' "$configuration" | grep '^salt_method' | cut -d'=' -f 2-) + config_salt=$(printf '%s' "$configuration" | grep '^config_salt' | cut -d'=' -f 2-) } # print this script's usage message to stderr @@ -1186,6 +1157,10 @@ help() { Method used to compute deterministic salt; can be password or configured defaults to password + -cm, --config_salt=CONFIG_SALT + If the salt method is "configured" then force it to use + this salt, otherwise it is randomly initialized. + --set-openssl-path=PATH_TO_OPENSSL use OpenSSL at this path; defaults to 'openssl' in \$PATH @@ -1291,10 +1266,10 @@ show_file='' uninstall='' upgrade='' openssl_path='openssl' - use_pbkdf2='' digest='' salt_method='' +config_salt='' # used to bypass certain safety checks requires_existing_config='' @@ -1360,6 +1335,13 @@ while [[ "${1:-}" != '' ]]; do --salt-method=*) salt_method=${1#*=} ;; + -cs | --config-salt) + config_salt=$2 + shift + ;; + --config-salt=*) + config_salt=${1#*=} + ;; -p | --password) password=$2 shift @@ -1503,11 +1485,14 @@ get_salt_method get_password if [[ "$salt_method" == "configured" ]]; then - extra_salt=$(_load_versioned_config_var "transcrypt.extra-salt") - # If we have not configured the extra salt (or we need to rekey), - # then generate new random salt - if [[ "$extra_salt" == "" ]] || [[ $rekey ]]; then - extra_salt=$(openssl rand -hex 32) + # If the user didnt specify explicitly, try to load the config salt + if [[ "$config_salt" == "" ]]; then + config_salt=$(_load_versioned_config_var "transcrypt.config-salt") + # If we have not configured the config_salt (or we need to rekey), + # then generate new random salt + if [[ "$config_salt" == "" ]] || [[ $rekey ]]; then + config_salt=$(openssl rand -hex 32) + fi fi fi From 10b53724927a63fd6ed3c1a7a7d4a713552c0b69 Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 6 May 2022 08:28:49 -0400 Subject: [PATCH 04/15] Script to enforce indentation pattern --- tools/fix_indentation.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tools/fix_indentation.py diff --git a/tools/fix_indentation.py b/tools/fix_indentation.py new file mode 100644 index 0000000..97b1c35 --- /dev/null +++ b/tools/fix_indentation.py @@ -0,0 +1,28 @@ +def main(): + import ubelt as ub + import xdev + fpath = ub.Path('$HOME/code/transcrypt/transcrypt').expand() + text = fpath.read_text() + lines = text.split('\n') + + tabstop = 4 + indent_pat = xdev.Pattern.from_regex(r'(\s*)(.*)') + space_pat = xdev.Pattern.from_regex(r' ' * tabstop) + + in_usage = 0 + new_lines = [] + for line in lines: + if 'cat <<-EOF' == line.strip(): + in_usage = True + if 'EOF' == line.strip(): + in_usage = False + indent, suffix = indent_pat.match(line).groups() + hist = ub.dict_hist(indent) + ntabs = hist.get('\t', 0) + if in_usage: + # Only have 2 leading tabs in the usage part + new_indent = space_pat.sub('\t', indent, count=(2 - ntabs)) + else: + new_indent = space_pat.sub('\t', indent) + new_line = new_indent + suffix + new_lines.append(new_line) From c714c614acb177e688b0a6298c905d42d9d1055b Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 6 May 2022 08:29:50 -0400 Subject: [PATCH 05/15] Fix tab indent --- tools/fix_indentation.py | 10 ++ transcrypt | 352 +++++++++++++++++++-------------------- 2 files changed, 186 insertions(+), 176 deletions(-) diff --git a/tools/fix_indentation.py b/tools/fix_indentation.py index 97b1c35..dfd895e 100644 --- a/tools/fix_indentation.py +++ b/tools/fix_indentation.py @@ -26,3 +26,13 @@ def main(): new_indent = space_pat.sub('\t', indent) new_line = new_indent + suffix new_lines.append(new_line) + + fpath.write_text('\n'.join(new_lines)) + + +if __name__ == '__main__': + """ + CommandLine: + python tools/fix_indentation.py + """ + main() diff --git a/transcrypt b/transcrypt index 3a416b2..b41b776 100755 --- a/transcrypt +++ b/transcrypt @@ -71,35 +71,35 @@ realpath() { # sets a bash global variable by name _set_global(){ - key=$1 - val=$2 - printf -v "$key" '%s' "$val" + key=$1 + val=$2 + printf -v "$key" '%s' "$val" } # Checks if the target variable is in the set of valid values. If it is not, it # unsets the target variable, then if not in interactive mode it calls die. _validate_variable_str(){ - local varname=$1 - local valid_values=$2 - local varval=${!varname} - if ! _is_contained_str "$varval" "$valid_values"; then - message=$(printf '%s is `%s`, but must be one of: %s' "$varname" "$varval" "$valid_values") + local varname=$1 + local valid_values=$2 + local varval=${!varname} + if ! _is_contained_str "$varval" "$valid_values"; then + message=$(printf '%s is `%s`, but must be one of: %s' "$varname" "$varval" "$valid_values") if [[ $interactive ]]; then - _set_global "$varname" "" - echo "$message" + _set_global "$varname" "" + echo "$message" else - die 1 "$message" - fi - fi + die 1 "$message" + fi + fi } # Helper to prompt the user, store a response, and validate the result _get_user_input() { - local varname=$1 - local default=$2 - local validate_fn=$3 - local prompt=$4 + local varname=$1 + local default=$2 + local validate_fn=$3 + local prompt=$4 while [[ ! ${!varname} ]]; do local answer= @@ -107,21 +107,21 @@ _get_user_input() { printf '%s' "$prompt" read -r answer fi - # use the default value if the user gave no answer; otherwise call the - # validate function, which should set the varname to empty if it is - # invalid and the user should continue, otherwise it should die. + # use the default value if the user gave no answer; otherwise call the + # validate function, which should set the varname to empty if it is + # invalid and the user should continue, otherwise it should die. if [[ ! $answer ]]; then - _set_global "$varname" "$default" + _set_global "$varname" "$default" else - _set_global "$varname" "$answer" - ${validate_fn} + _set_global "$varname" "$answer" + ${validate_fn} fi done } # compatible openssl list command _openssl_list(){ - arg=$1 + arg=$1 if "${openssl_path} list-$arg" &>/dev/null; then # OpenSSL < v1.1.0 "${openssl_path}" "list-$arg" @@ -133,9 +133,9 @@ _openssl_list(){ # Check if the first arg is contained in the space separated second arg _is_contained_str(){ - arg=$1 - values=$2 - echo "$values" | tr -s ' ' '\n' | grep -Fx "$arg" &>/dev/null + arg=$1 + values=$2 + echo "$values" | tr -s ' ' '\n' | grep -Fx "$arg" &>/dev/null } @@ -143,88 +143,88 @@ _is_contained_str(){ # shellcheck disable=SC2155 _load_versioned_config_var(){ # the current git repository's top-level directory - if [ -z "${VERSIONED_TC_CONFIG+x}" ]; then - readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) - # This is the place where transcrypt can store state that will be - # "versioned" (i.e. checked into the repo) - readonly VERSIONED_TC_DIR="${REPO}/.transcrypt" - readonly VERSIONED_TC_CONFIG="${VERSIONED_TC_DIR}/config" - fi - local key=$1 - # Test for blocked variables that should not go into a plaintext config file - if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then - warn "Cannot use ${key} in versioned the transcrypt config" - return 1 - fi - git config --file "${VERSIONED_TC_CONFIG}" --get "${key}" || true + if [ -z "${VERSIONED_TC_CONFIG+x}" ]; then + readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) + # This is the place where transcrypt can store state that will be + # "versioned" (i.e. checked into the repo) + readonly VERSIONED_TC_DIR="${REPO}/.transcrypt" + readonly VERSIONED_TC_CONFIG="${VERSIONED_TC_DIR}/config" + fi + local key=$1 + # Test for blocked variables that should not go into a plaintext config file + if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then + warn "Cannot use ${key} in versioned the transcrypt config" + return 1 + fi + git config --file "${VERSIONED_TC_CONFIG}" --get "${key}" || true } # Write a config var to the versioned config _set_versioned_config_var(){ - local key=$1 - local val=$2 - # Test for blocked variables that should not go into a plaintext config file - if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then - warn "Cannot use ${key} in versioned the transcrypt config" - return 1 - fi - mkdir -p "${VERSIONED_TC_DIR}" - git config --file "${VERSIONED_TC_CONFIG}" "${key}" "${val}" + local key=$1 + local val=$2 + # Test for blocked variables that should not go into a plaintext config file + if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then + warn "Cannot use ${key} in versioned the transcrypt config" + return 1 + fi + mkdir -p "${VERSIONED_TC_DIR}" + git config --file "${VERSIONED_TC_CONFIG}" "${key}" "${val}" } # _load_config_var(){ - # First try loading from the local checkout-independent .git/config file - # If that doesn't work, then look in the .transcrypt/config file - # (which is expected to be stored in plaintext and checked into the repo) - # Certain values will be blocked from being placed here (like the password) - local key=$1 - git config --get --local "${key}" + # First try loading from the local checkout-independent .git/config file + # If that doesn't work, then look in the .transcrypt/config file + # (which is expected to be stored in plaintext and checked into the repo) + # Certain values will be blocked from being placed here (like the password) + local key=$1 + git config --get --local "${key}" if [[ "$?" != "0" ]]; then - _load_versioned_config_var "${key}" + _load_versioned_config_var "${key}" fi } # shellcheck disable=SC2155 _load_transcrypt_config_vars(){ - # Populate bash vars with our config + # Populate bash vars with our config cipher=$(git config --get --local transcrypt.cipher) - digest=$(git config --get --local transcrypt.digest) - use_pbkdf2=$(git config --get --local transcrypt.use-pbkdf2) - salt_method=$(git config --get --local transcrypt.salt-method) + digest=$(git config --get --local transcrypt.digest) + use_pbkdf2=$(git config --get --local transcrypt.use-pbkdf2) + salt_method=$(git config --get --local transcrypt.salt-method) password=$(git config --get --local transcrypt.password) openssl_path=$(git config --get --local transcrypt.openssl-path) - if [[ "$salt_method" == "configured" ]]; then - config_salt=$(_load_versioned_config_var "transcrypt.config-salt") - else - config_salt="" - fi + if [[ "$salt_method" == "configured" ]]; then + config_salt=$(_load_versioned_config_var "transcrypt.config-salt") + else + config_salt="" + fi } _load_vars_for_encryption(){ - # Helper to populate variables needed to call openssl encryption - _load_transcrypt_config_vars - - if [[ "$use_pbkdf2" == "1" ]]; then - pbkdf2_args=('-pbkdf2') - else - pbkdf2_args=() - fi - - if [[ "$salt_method" == "password" ]]; then - extra_salt=$password - elif [[ "$salt_method" == "configured" ]]; then - extra_salt=$config_salt - else - die "unknown salt method" - fi - - if [[ "$extra_salt" == "" ]]; then - die "Extra salt is not set" - fi + # Helper to populate variables needed to call openssl encryption + _load_transcrypt_config_vars + + if [[ "$use_pbkdf2" == "1" ]]; then + pbkdf2_args=('-pbkdf2') + else + pbkdf2_args=() + fi + + if [[ "$salt_method" == "password" ]]; then + extra_salt=$password + elif [[ "$salt_method" == "configured" ]]; then + extra_salt=$config_salt + else + die "unknown salt method" + fi + + if [[ "$extra_salt" == "" ]]; then + die "Extra salt is not set" + fi } @@ -268,12 +268,12 @@ gather_repo_metadata() { readonly GIT_ATTRIBUTES="${REPO}/.gitattributes" fi - # This is the place where transcrypt can store state that will be - # "versioned" (i.e. checked into the repo) - readonly RELATIVE_VERSIONED_TC_DIR=".transcrypt" - readonly RELATIVE_VERSIONED_TC_CONFIG="${RELATIVE_VERSIONED_TC_DIR}/config" - readonly VERSIONED_TC_DIR="${REPO}/${RELATIVE_VERSIONED_TC_DIR}" - readonly VERSIONED_TC_CONFIG="${REPO}/${RELATIVE_VERSIONED_TC_CONFIG}" + # This is the place where transcrypt can store state that will be + # "versioned" (i.e. checked into the repo) + readonly RELATIVE_VERSIONED_TC_DIR=".transcrypt" + readonly RELATIVE_VERSIONED_TC_CONFIG="${RELATIVE_VERSIONED_TC_DIR}/config" + readonly VERSIONED_TC_DIR="${REPO}/${RELATIVE_VERSIONED_TC_DIR}" + readonly VERSIONED_TC_CONFIG="${REPO}/${RELATIVE_VERSIONED_TC_CONFIG}" } # print a message to stderr @@ -332,8 +332,8 @@ git_clean() { else _load_vars_for_encryption # NOTE: salt must be 16 bytes, its openssl standard - salt=$("$openssl_path" dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16) - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + salt=$("$openssl_path" dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16) + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" fi } @@ -343,7 +343,7 @@ git_smudge() { tempfile=$(mktemp 2>/dev/null || mktemp -t tmp) trap 'rm -f "$tempfile"' EXIT #_load_transcrypt_config_vars - _load_vars_for_encryption + _load_vars_for_encryption tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a 2>/dev/null || cat "$tempfile" } @@ -419,7 +419,7 @@ git_pre_commit() { : # Do nothing # The first bytes of an encrypted file must be "Salted" in Base64 elif [[ $firstbytes != "U2FsdGVk" ]]; then - echo "firstbytes = $firstbytes" + echo "firstbytes = $firstbytes" 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 @@ -508,52 +508,52 @@ run_safety_checks() { # unset the cipher variable if it is not supported by openssl validate_cipher() { - local valid_ciphers - valid_ciphers=$(_openssl_list cipher-commands) - _validate_variable_str "cipher" "$valid_ciphers" + local valid_ciphers + valid_ciphers=$(_openssl_list cipher-commands) + _validate_variable_str "cipher" "$valid_ciphers" } validate_digest() { - local valid_digests - valid_digests=$(_openssl_list digest-commands) - _validate_variable_str "digest" "$valid_digests" + local valid_digests + valid_digests=$(_openssl_list digest-commands) + _validate_variable_str "digest" "$valid_digests" } validate_use_pbkdf2() { - _validate_variable_str "use_pbkdf2" "0 1" + _validate_variable_str "use_pbkdf2" "0 1" } validate_salt_method(){ - _validate_variable_str "salt_method" "$VALID_SALT_METHODS" + _validate_variable_str "salt_method" "$VALID_SALT_METHODS" } # ensure we have a digest to hash the salted password get_digest() { - local prompt - prompt=$(printf 'Encrypt using which digest? [%s] ' "$DEFAULT_DIGEST") - _get_user_input digest "$DEFAULT_DIGEST" "validate_digest" "$prompt" + local prompt + prompt=$(printf 'Encrypt using which digest? [%s] ' "$DEFAULT_DIGEST") + _get_user_input digest "$DEFAULT_DIGEST" "validate_digest" "$prompt" } # ensure we have a cipher to encrypt with get_cipher() { - local prompt - prompt=$(printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER") - _get_user_input cipher "$DEFAULT_CIPHER" "validate_cipher" "$prompt" + local prompt + prompt=$(printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER") + _get_user_input cipher "$DEFAULT_CIPHER" "validate_cipher" "$prompt" } get_use_pbkdf2() { - local prompt - prompt=$(printf 'Use pbkdf2? [%s] ' "$DEFAULT_USE_PBKDF2") - _get_user_input use_pbkdf2 "$DEFAULT_USE_PBKDF2" "validate_use_pbkdf2" "$prompt" + local prompt + prompt=$(printf 'Use pbkdf2? [%s] ' "$DEFAULT_USE_PBKDF2") + _get_user_input use_pbkdf2 "$DEFAULT_USE_PBKDF2" "validate_use_pbkdf2" "$prompt" } get_salt_method() { - local prompt - prompt=$(printf 'Compute salt using which method? [%s] ' "$DEFAULT_SALT_METHOD") - _get_user_input salt_method "$DEFAULT_SALT_METHOD" "validate_salt_method" "$prompt" + local prompt + prompt=$(printf 'Compute salt using which method? [%s] ' "$DEFAULT_SALT_METHOD") + _get_user_input salt_method "$DEFAULT_SALT_METHOD" "validate_salt_method" "$prompt" } # ensure we have a password to encrypt with @@ -586,9 +586,9 @@ get_password() { confirm_configuration() { local answer= - _display_git_configuration + _display_git_configuration printf 'The following configuration will be saved:\n\n' - _display_runtime_configuration + _display_runtime_configuration printf 'Does this look correct? [Y/n] ' read -r -n 1 -s answer @@ -605,9 +605,9 @@ confirm_configuration() { confirm_rekey() { local answer= - _display_git_configuration + _display_git_configuration printf 'The following configuration will be saved:\n\n' - _display_runtime_configuration + _display_runtime_configuration 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] ' @@ -644,12 +644,12 @@ save_helper_scripts() { local current_transcrypt current_transcrypt=$(realpath "$0" 2>/dev/null) - if [[ "$EDITABLE_INSTALL" == "1" ]]; then - # Editable mode is for debugging - ln -fs "$current_transcrypt" "${CRYPT_DIR}/transcrypt" - else - cp "$current_transcrypt" "${CRYPT_DIR}/transcrypt" - fi + if [[ "$EDITABLE_INSTALL" == "1" ]]; then + # Editable mode is for debugging + ln -fs "$current_transcrypt" "${CRYPT_DIR}/transcrypt" + else + cp "$current_transcrypt" "${CRYPT_DIR}/transcrypt" + fi # make scripts executable for script in {transcrypt,}; do chmod 0755 "${CRYPT_DIR}/${script}" @@ -699,30 +699,30 @@ save_configuration() { git config transcrypt.password "$password" - # TODO: We may want to allow repo settings to be stored here as well. - #if [[ "$use_versioned_config" == "1" ]]; then - # _set_versioned_config_var "transcrypt.version" "$VERSION" - # _set_versioned_config_var "transcrypt.cipher" "$cipher" - # _set_versioned_config_var "transcrypt.digest" "$digest" - # _set_versioned_config_var "transcrypt.use-pbkdf2" "$use_pbkdf2" - # _set_versioned_config_var "transcrypt.salt-method" "$salt_method" - #fi - if [[ "$salt_method" == "configured" ]]; then - # TODO: we may want to also write the config-salt variable to the local config - # The user might not care about cross machine transparency - git config transcrypt.config-salt "$config_salt" - _set_versioned_config_var "transcrypt.config-salt" "$config_salt" - if ! git ls-files --error-unmatch "$RELATIVE_VERSIONED_TC_CONFIG" > /dev/null 2>&1; then - git add "${RELATIVE_VERSIONED_TC_CONFIG}" - printf "*** The contents of %s were configured. ***\n" "${RELATIVE_VERSIONED_TC_CONFIG}" - printf '*** COMMIT THIS CHANGE RIGHT AWAY! ***\n\n' - fi - if ! git diff --exit-code "$RELATIVE_VERSIONED_TC_CONFIG" > /dev/null 2>&1; then - git add "${RELATIVE_VERSIONED_TC_CONFIG}" - printf "*** The contents of %s were updated. ***\n" "${RELATIVE_VERSIONED_TC_CONFIG}" - printf '*** COMMIT THIS CHANGE RIGHT AWAY! ***\n\n' - fi - fi + # TODO: We may want to allow repo settings to be stored here as well. + #if [[ "$use_versioned_config" == "1" ]]; then + # _set_versioned_config_var "transcrypt.version" "$VERSION" + # _set_versioned_config_var "transcrypt.cipher" "$cipher" + # _set_versioned_config_var "transcrypt.digest" "$digest" + # _set_versioned_config_var "transcrypt.use-pbkdf2" "$use_pbkdf2" + # _set_versioned_config_var "transcrypt.salt-method" "$salt_method" + #fi + if [[ "$salt_method" == "configured" ]]; then + # TODO: we may want to also write the config-salt variable to the local config + # The user might not care about cross machine transparency + git config transcrypt.config-salt "$config_salt" + _set_versioned_config_var "transcrypt.config-salt" "$config_salt" + if ! git ls-files --error-unmatch "$RELATIVE_VERSIONED_TC_CONFIG" > /dev/null 2>&1; then + git add "${RELATIVE_VERSIONED_TC_CONFIG}" + printf "*** The contents of %s were configured. ***\n" "${RELATIVE_VERSIONED_TC_CONFIG}" + printf '*** COMMIT THIS CHANGE RIGHT AWAY! ***\n\n' + fi + if ! git diff --exit-code "$RELATIVE_VERSIONED_TC_CONFIG" > /dev/null 2>&1; then + git add "${RELATIVE_VERSIONED_TC_CONFIG}" + printf "*** The contents of %s were updated. ***\n" "${RELATIVE_VERSIONED_TC_CONFIG}" + printf '*** COMMIT THIS CHANGE RIGHT AWAY! ***\n\n' + fi + fi # write the filter settings. Sorry for the horrific quote escaping below... # shellcheck disable=SC2016 @@ -756,24 +756,24 @@ _display_runtime_configuration(){ printf ' DIGEST: %s\n' "$digest" printf ' USE_PBKDF2: %s\n' "$use_pbkdf2" printf ' SALT_METHOD: %s\n' "$salt_method" - if [[ "$salt_method" == "configured" ]]; then - printf ' CONFIG_SALT: %s\n' "$config_salt" - fi + if [[ "$salt_method" == "configured" ]]; then + printf ' CONFIG_SALT: %s\n' "$config_salt" + fi printf ' CIPHER: %s\n' "$cipher" printf ' PASSWORD: %s\n\n' "$password" } # display the current configuration settings display_configuration() { - _load_transcrypt_config_vars + _load_transcrypt_config_vars local escaped_password=${password//\'/\'\\\'\'} printf 'The current repository was configured using transcrypt version %s\n' "$CONFIGURED" printf 'and has the following configuration:\n\n' - _display_git_configuration - _display_runtime_configuration + _display_git_configuration + _display_runtime_configuration printf 'Copy and paste the following command to initialize a cloned repository:\n\n' printf " transcrypt -c '%s' -p '%s' -md '%s' --use-pbkdf2 '%s' -sm '%s' -cs '%s'\n" \ - "$cipher" "$escaped_password" "$digest" "$use_pbkdf2" "$salt_method" "$config_salt" + "$cipher" "$escaped_password" "$digest" "$use_pbkdf2" "$salt_method" "$config_salt" #[[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO" #printf ' GIT_DIR: %s\n' "$GIT_DIR" @@ -781,14 +781,14 @@ display_configuration() { #printf ' DIGEST: %s\n' "$current_digest" #printf ' USE_PBKDF2: %s\n' "$current_use_pbkdf2" #printf ' SALT_METHOD: %s\n' "$current_salt_method" - #if [[ "$current_salt_method" == "configured" ]]; then - # printf ' CONFIG_SALT: %s\n' "$current_config_salt" - #fi + #if [[ "$current_salt_method" == "configured" ]]; then + # printf ' CONFIG_SALT: %s\n' "$current_config_salt" + #fi #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' -md '%s' --use_pbkdf2 '%s' -sm '%s'\n" \ - # "$current_cipher" "$escaped_password" "$current_digest" "$current_use_pbkdf2" "$current_salt_method" + # "$current_cipher" "$escaped_password" "$current_digest" "$current_use_pbkdf2" "$current_salt_method" } # remove transcrypt-related settings from the repository's git config @@ -1058,7 +1058,7 @@ export_gpg() { die 1 'GPG recipient key "%s" does not exist' "$gpg_recipient" fi - _load_transcrypt_config_vars + _load_transcrypt_config_vars #local current_cipher #current_cipher=$(git config --get --local transcrypt.cipher) @@ -1068,9 +1068,9 @@ export_gpg() { 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 >"${CRYPT_DIR}/${gpg_recipient}.asc" - printf 'password=%s\ncipher=%s\ndigest=%s\nuse_pbkdf2=%s\nsalt_method=%s\nconfig_salt=%s\n\n' \ - "$password" "$cipher" "$digest" "$use_pbkdf2" "$salt_method" "$config_salt" | \ - $gpg_encrypt_cmd >"${CRYPT_DIR}/${gpg_recipient}.asc" + printf 'password=%s\ncipher=%s\ndigest=%s\nuse_pbkdf2=%s\nsalt_method=%s\nconfig_salt=%s\n\n' \ + "$password" "$cipher" "$digest" "$use_pbkdf2" "$salt_method" "$config_salt" | \ + $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" } @@ -1158,8 +1158,8 @@ help() { defaults to password -cm, --config_salt=CONFIG_SALT - If the salt method is "configured" then force it to use - this salt, otherwise it is randomly initialized. + If the salt method is "configured" then force it to use + this salt, otherwise it is randomly initialized. --set-openssl-path=PATH_TO_OPENSSL use OpenSSL at this path; defaults to 'openssl' in \$PATH @@ -1485,15 +1485,15 @@ get_salt_method get_password if [[ "$salt_method" == "configured" ]]; then - # If the user didnt specify explicitly, try to load the config salt - if [[ "$config_salt" == "" ]]; then - config_salt=$(_load_versioned_config_var "transcrypt.config-salt") - # If we have not configured the config_salt (or we need to rekey), - # then generate new random salt - if [[ "$config_salt" == "" ]] || [[ $rekey ]]; then - config_salt=$(openssl rand -hex 32) - fi - fi + # If the user didnt specify explicitly, try to load the config salt + if [[ "$config_salt" == "" ]]; then + config_salt=$(_load_versioned_config_var "transcrypt.config-salt") + # If we have not configured the config_salt (or we need to rekey), + # then generate new random salt + if [[ "$config_salt" == "" ]] || [[ $rekey ]]; then + config_salt=$(openssl rand -hex 32) + fi + fi fi if [[ $rekey ]] && [[ $interactive ]]; then From 0496988026f76a447e664433ff78301d7634c917 Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 6 May 2022 16:35:56 -0400 Subject: [PATCH 06/15] Test rekey, and expand python API --- tests/test_transcrypt.py | 386 +++++++++++++++++++++++++++++++-------- transcrypt | 2 +- 2 files changed, 310 insertions(+), 78 deletions(-) diff --git a/tests/test_transcrypt.py b/tests/test_transcrypt.py index 6467865..34c8964 100644 --- a/tests/test_transcrypt.py +++ b/tests/test_transcrypt.py @@ -1,97 +1,237 @@ """ Requirements: - pip install gpg_lite pip install ubelt + pip install gpg_lite + pip install GitPython """ import ubelt as ub -import os +import base64 + +SALTED = base64.b64encode(b'Salted').decode('utf8') -SALTED = 'U2FsdGV' +class Transcrypt(ub.NiceRepr): + """ + A Python wrapper around the Transcrypt API -class TranscryptAPI: + Example: + >>> import sys, ubelt + >>> sys.path.append(ubelt.expandpath('~/code/transcrypt/tests')) + >>> from test_transcrypt import * # NOQA + >>> sandbox = DemoSandbox(verbose=0).setup() + >>> config = {'digest': 'sha256', + >>> 'use_pbkdf2': '1', + >>> 'config_salt': '665896be121e1a0a4a7b18f01780061', + >>> 'salt_method': 'configured'} + >>> self = Transcrypt(sandbox.repo_dpath, + >>> config=config, env=sandbox.env, verbose=0) + >>> print(self.version()) + >>> self.config['password'] = 'chbs' + >>> self.login() + >>> print(self.display()) + >>> secret_fpath1 = self.dpath / 'safe/secret1.txt' + >>> secret_fpath2 = self.dpath / 'safe/secret2.txt' + >>> secret_fpath3 = self.dpath / 'safe/secret3.txt' + >>> secret_fpath1.write_text('secret message 1') + >>> secret_fpath2.write_text('secret message 2') + >>> secret_fpath3.write_text('secret message 3') + >>> sandbox.git.add(secret_fpath1, secret_fpath2, secret_fpath3) + >>> sandbox.git.commit('-am add secret messages') + >>> encrypted_paths = self.list() + >>> assert len(encrypted_paths) == 3 + >>> assert self.show_raw(secret_fpath1) == 'U2FsdGVkX18147KP5UmqOFywveuOGf4hCwrWpfJDp3Ah0HHbFPEGdJE0kM4npWzI' + >>> assert self.show_raw(secret_fpath2) == 'U2FsdGVkX183LEAwwnJ0ne/OKU5VANJsOqCA92Oi9hVkKHIwZYiCgJOoedoShPj7' + >>> assert self.show_raw(secret_fpath3) == 'U2FsdGVkX1/NdLm6twCdF3xYLPCfXacDNsHEeGq0UBC1fwTlJKnN2KmPysS/ylPj' + """ default_config = { 'cipher': 'aes-256-cbc', - 'password': 'correct horse battery staple', + 'password': None, 'digest': 'md5', 'use_pbkdf2': '0', 'salt_method': 'password', 'config_salt': '', } - def __init__(self, dpath, config=None, verbose=2, transcript_exe=None): + def __init__(self, dpath, config=None, env=None, transcript_exe=None, verbose=0): self.dpath = dpath self.verbose = verbose self.transcript_exe = ub.Path(ub.find_exe('transcrypt')) self.env = {} self.config = self.default_config.copy() + if env is not None: + self.env.update(env) if config: self.config.update(config) - def cmd(self, command, shell=False): - return ub.cmd(command, cwd=self.dpath, verbose=self.verbose, - env=self.env, shell=shell) + def __nice__(self): + return '{}, {}'.format(self.dpath, ub.repr2(self.config)) + + def _cmd(self, command, shell=False, check=True, verbose=None): + """ + Helper to execute underlying transcrypt commands + """ + if verbose is None: + verbose = self.verbose + return ub.cmd(command, cwd=self.dpath, verbose=verbose, env=self.env, + shell=shell, check=check) + + def _config_args(self): + arg_templates = [ + "-c", self.config['cipher'], + "-p", self.config['password'], + "-md", self.config['digest'], + "--use-pbkdf2", self.config['use_pbkdf2'], + "-sm", self.config['salt_method'], + "-cs", self.config['config_salt'], + ] + args = [template.format(**self.config) for template in arg_templates] + return args + + def is_configured(self): + """ + Determine if the transcrypt credentials are populated in the repo + + Returns: + bool : True if the repo is configured with credentials + """ + info = self._cmd(f'{self.transcript_exe} -d', check=0, verbose=0) + return info['ret'] == 0 def login(self): - command = ( - "{transcript_exe} -c '{cipher}' -p '{password}' " - "-md '{digest}' --use-pbkdf2 '{use_pbkdf2}' " - "-sm '{salt_method}' " - "-cs '{config_salt}' " - "-y" - ).format(transcript_exe=self.transcript_exe, **self.config) - self.cmd(command) + """ + Configure credentials + """ + args = self._config_args() + command = [str(self.transcript_exe), *args, '-y'] + self._cmd(command) def logout(self): - self.cmd(f'{self.transcript_exe} -f -y') + """ + Flush credentials + """ + self._cmd(f'{self.transcript_exe} -f -y') + + def rekey(self, new_config): + """ + Re-encrypt all encrypted files using new credentials + """ + self.config.update(new_config) + args = self._config_args() + command = [str(self.transcript_exe), '--rekey', *args, '-y'] + self._cmd(command) def display(self): - self.cmd(f'{self.transcript_exe} -d') + """ + Returns: + str: the configuration details of the repo + """ + return self._cmd(f'{self.transcript_exe} -d')['out'].rstrip() + + def version(self): + """ + Returns: + str: the version + """ + return self._cmd(f'{self.transcript_exe} --version')['out'].rstrip() + + def _crypt_dir(self): + info = self._cmd('git config --local transcrypt.crypt-dir', check=0) + if info['err'] == 0: + crypt_dpath = ub.Path(info['out'].strip()) + else: + crypt_dpath = self.dpath / '.git/crypt' + return crypt_dpath def export_gpg(self, recipient): - self.cmd(f'{self.transcript_exe} --export-gpg "{recipient}"') - self.crypt_dpath = self.cmd('git config --local transcrypt.crypt-dir')['out'] or self.dpath / '.git/crypt' - asc_fpath = (self.crypt_dpath / (recipient + '.asc')) + """ + Encode the transcrypt credentials securely in an encrypted gpg message + + Returns: + Path: path to the gpg encrypted file containing the repo config + """ + self._cmd(f'{self.transcript_exe} --export-gpg "{recipient}"') + crypt_dpath = self._crypt_dir() + asc_fpath = (crypt_dpath / (recipient + '.asc')) return asc_fpath def import_gpg(self, asc_fpath): + """ + Configure the repo using a given gpg encrypted file + """ command = f"{self.transcript_exe} --import-gpg '{asc_fpath}' -y" - self.cmd(command) + self._cmd(command) def show_raw(self, fpath): - return self.cmd(f'{self.transcript_exe} -s {fpath}')['out'] - - def _manual_hack_info(self): """ - Info on how to get an env to run a failing command manually + Show the encrypted contents of a file that will be publicly viewable """ - for k, v in self.env.items(): - print(f'export {k}={v}') - print(f'cd {self.dpath}') + return self._cmd(f'{self.transcript_exe} -s {fpath}')['out'].rstrip() + def list(self): + """ + Returns: + List[str]: relative paths of all files managed by transcrypt + """ + result = self._cmd(f'{self.transcript_exe} --list')['out'].rstrip() + paths = result.split('\n') + return paths -class TestEnvironment: + def uninstall(self): + """ + Flushes credentials and removes transcrypt files + """ + return self._cmd(f'{self.transcript_exe} --uninstall -y') - def __init__(self, dpath=None, config=None, verbose=2): + def upgrade(self): + """ + Upgrades a configured repo to "this" version of transcrypt + """ + return self._cmd(f'{self.transcript_exe} --upgrade -y') + + def _load_local_config(self): + local_config = { + 'cipher': self._cmd('git config --get --local transcrypt.cipher')['out'].strip(), + 'digest': self._cmd('git config --get --local transcrypt.digest')['out'].strip(), + 'use_pbkdf2': self._cmd('git config --get --local transcrypt.use-pbkdf2')['out'].strip(), + 'salt_method': self._cmd('git config --get --local transcrypt.salt-method')['out'].strip(), + 'password': self._cmd('git config --get --local transcrypt.password')['out'].strip(), + 'openssl_path': self._cmd('git config --get --local transcrypt.openssl-path')['out'].strip(), + } + if local_config['salt_method'] == 'configured': + tc_config_path = self.dpath / '.transcrypt/config' + local_config['config_salt'] = self._cmd(f'git config --get --file {tc_config_path} transcrypt.config-salt')['out'].strip() + return local_config + + +class DemoSandbox(ub.NiceRepr): + """ + A environment for demo / testing of the transcrypt API + """ + def __init__(self, dpath=None, verbose=0): if dpath is None: - # import tempfile - # self._tmpdir = tempfile.TemporaryDirectory() - # dpath = self._tmpdir.name - dpath = ub.Path.appdir('transcrypt/tests/test_env') + import tempfile + self._tmpdir = tempfile.TemporaryDirectory() + dpath = self._tmpdir.name + # dpath = ub.Path.appdir('transcrypt/tests/test_env') + self.env = {} self.dpath = ub.Path(dpath) self.gpg_store = None self.repo_dpath = None + self.git = None self.verbose = verbose - self.tc = None - self.config = config + + def __nice__(self): + return str(self.dpath) def setup(self): - self._setup_gpg() - self._setup_git() - self._setup_transcrypt() + self._setup_gpghome() + self._setup_gitrepo() + self._setup_contents() return self - def _setup_gpg(self): + def _setup_gpghome(self): + if self.verbose: + print('setup sandbox gpghome') import gpg_lite self.gpg_home = (self.dpath / 'gpg').ensuredir() self.gpg_store = gpg_lite.GPGStore( @@ -109,10 +249,13 @@ def _setup_gpg(self): # Fix GNUPG permissions (self.gpg_home / 'private-keys-v1.d').ensuredir() # 600 for files and 700 for directories - ub.cmd('find ' + str(self.gpg_home) + r' -type f -exec chmod 600 {} \;', shell=True, verbose=self.verbose, cwd=self.gpg_home) - ub.cmd('find ' + str(self.gpg_home) + r' -type d -exec chmod 700 {} \;', shell=True, verbose=self.verbose, cwd=self.gpg_home) + ub.cmd('find ' + str(self.gpg_home) + r' -type f -exec chmod 600 {} \;', shell=True, cwd=self.gpg_home) + ub.cmd('find ' + str(self.gpg_home) + r' -type d -exec chmod 700 {} \;', shell=True, cwd=self.gpg_home) + self.env['GNUPGHOME'] = str(self.gpg_home) - def _setup_git(self): + def _setup_gitrepo(self): + if self.verbose: + print('setup sandbox gitrepo') import git # Make a git repo and add some public content repo_name = 'demo-repo' @@ -125,6 +268,10 @@ def _setup_git(self): self.git = git.Git(self.repo_dpath) self.git.init() + + def _setup_contents(self): + if self.verbose: + print('setup sandbox git contents') readme_fpath = (self.repo_dpath / 'README.md') readme_fpath.write_text('content') self.git.add(readme_fpath) @@ -139,43 +286,68 @@ def _setup_git(self): self.git.commit('-am Add initial contents') self.safe_dpath = (self.repo_dpath / 'safe').ensuredir() self.secret_fpath = self.safe_dpath / 'secret.txt' + self.secret_fpath.write_text('secret content') + + def _manual_hack_info(self): + """ + Info on how to get an env to run a failing command manually + """ + for k, v in self.env.items(): + print(f'export {k}={v}') + print(f'cd {self.repo_dpath}') + + +class TestCases: + """ + Unit tests to be applied to different transcrypt configurations + """ + + def __init__(self, config, verbose=0): + self.config = config + self.verbose = verbose + self.sandbox = None + self.tc = None - def _setup_transcrypt(self): - self.tc = TranscryptAPI(self.repo_dpath, self.config, - verbose=self.verbose) - err = self.tc.cmd(f'{self.tc.transcript_exe} -d')['err'].strip() - if err != 'transcrypt: the current repository is not configured': - raise AssertionError(f"Got {err}") + def setup(self): + self.sandbox = DemoSandbox(verbose=self.verbose) + self.sandbox.setup() + self.tc = Transcrypt( + dpath=self.sandbox.repo_dpath, + config=self.config, + env=self.sandbox.env, + verbose=self.verbose, + ) + assert not self.tc.is_configured() self.tc.login() - self.secret_fpath.write_text('secret content') - self.git.add(self.secret_fpath) - self.git.commit('-am add secret') + secret_fpath = self.sandbox.secret_fpath + self.sandbox.git.add(secret_fpath) + self.sandbox.git.commit('-am add secret') self.tc.display() - if self.gpg_home is not None: - self.tc.env['GNUPGHOME'] = str(self.gpg_home) def test_round_trip(self): - ciphertext = self.tc.show_raw(self.secret_fpath) - plaintext = self.secret_fpath.read_text() + secret_fpath = self.sandbox.secret_fpath + ciphertext = self.tc.show_raw(secret_fpath) + plaintext = secret_fpath.read_text() assert ciphertext.startswith(SALTED) assert plaintext.startswith('secret content') assert not plaintext.startswith(SALTED) self.tc.logout() - logged_out_text = self.secret_fpath.read_text() + logged_out_text = secret_fpath.read_text().rstrip() assert logged_out_text == ciphertext self.tc.login() - logged_in_text = self.secret_fpath.read_text() + logged_in_text = secret_fpath.read_text().rstrip() assert logged_out_text == ciphertext assert logged_in_text == plaintext def test_export_gpg(self): self.tc.display() - asc_fpath = self.tc.export_gpg(self.gpg_fpr) + recipient = self.sandbox.gpg_fpr + asc_fpath = self.tc.export_gpg(recipient) - info = self.tc.cmd(f'gpg --batch --quiet --decrypt "{asc_fpath}"') + info = self.tc._cmd(f'gpg --batch --quiet --decrypt "{asc_fpath}"') content = info['out'] got_config = dict([p.split('=', 1) for p in content.split('\n') if p]) @@ -197,24 +369,84 @@ def test_export_gpg(self): print(f'config={config}') raise AssertionError - # content = io.StringIO() - # with open(asc_fpath, 'r') as file: - # ciphertext = file.read() - # self.gpg_store.decrypt(ciphertext, content) - assert asc_fpath.exists() self.tc.logout() self.tc.import_gpg(asc_fpath) - plaintext = self.secret_fpath.read_text() + secret_fpath = self.sandbox.secret_fpath + plaintext = secret_fpath.read_text() assert plaintext.startswith('secret content') def test_rekey(self): # TODO - pass + new_config = { + 'cipher': 'aes-256-cbc', + 'password': '12345', + 'digest': 'sha256', + 'use_pbkdf2': '1', + 'salt_method': 'configured', + } + raw_before = self.tc.show_raw(self.sandbox.secret_fpath) + self.tc.rekey(new_config) + self.sandbox.git.commit('-am commit rekey') + raw_after = self.tc.show_raw(self.sandbox.secret_fpath) + assert raw_before != raw_after + + +def test_legacy_defaults(): + config = { + 'cipher': 'aes-256-cbc', + 'password': 'correct horse battery staple', + 'digest': 'md5', + 'use_pbkdf2': '0', + 'salt_method': 'password', + } + verbose = 1 + self = TestCases(config=config, verbose=verbose) + self.setup() + self.test_round_trip() + self.test_export_gpg() -def run_tests(): +def test_secure_defaults(): + config = { + 'cipher': 'aes-256-cbc', + 'password': 'correct horse battery staple', + 'digest': 'sha512', + 'use_pbkdf2': '1', + 'salt_method': 'configured', + } + verbose = 1 + self = TestCases(config=config, verbose=verbose) + self.setup() + self.test_round_trip() + self.test_export_gpg() + + +def test_configured_salt_changes_on_rekey(): + config = { + 'cipher': 'aes-256-cbc', + 'password': 'correct horse battery staple', + 'digest': 'sha512', + 'use_pbkdf2': '1', + 'salt_method': 'configured', + } + verbose = 1 + self = TestCases(config=config, verbose=verbose) + self.setup() + before_config = self.tc._load_local_config() + self.tc.rekey({'password': '12345', 'config_salt': ''}) + self.sandbox.git.commit('-am commit rekey') + after_config = self.tc._load_local_config() + assert before_config['config_salt'] != after_config['config_salt'] + assert before_config['password'] != after_config['password'] + assert before_config['cipher'] == after_config['cipher'] + assert before_config['use_pbkdf2'] == after_config['use_pbkdf2'] + assert before_config['salt_method'] == after_config['salt_method'] + assert before_config['openssl_path'] == after_config['openssl_path'] + + +def test_configuration_grid(): """ CommandLine: xdoctest -m /home/joncrall/code/transcrypt/tests/test_transcrypt.py run_tests @@ -223,7 +455,7 @@ def run_tests(): >>> import sys, ubelt >>> sys.path.append(ubelt.expandpath('~/code/transcrypt/tests')) >>> from test_transcrypt import * # NOQA - >>> self = TestEnvironment() + >>> self = DemoSandbox() >>> self.setup() >>> self.tc._manual_hack_info() >>> self.test_round_trip() @@ -236,20 +468,20 @@ def run_tests(): self = TestEnvironment(config={'use_pbkdf2': 1}) """ - # Test that transcrypt works under a variety of config conditions + basis = { - 'cipher': ['aes-256-cbc'], + 'cipher': ['aes-256-cbc', 'aes-128-ecb'], 'password': ['correct horse battery staple'], 'digest': ['md5', 'sha256'], 'use_pbkdf2': ['0', '1'], 'salt_method': ['password', 'configured'], 'config_salt': ['', 'mylittlecustomsalt'], } - - for params in ub.named_product(basis): + test_grid = list(ub.named_product(basis)) + for params in ub.ProgIter(test_grid, desc='test configs'): config = params.copy() - self = TestEnvironment(config=config) + self = TestCases(config=config) self.setup() self.test_round_trip() self.test_export_gpg() @@ -260,4 +492,4 @@ def run_tests(): CommandLine: python ~/code/transcrypt/tests/test_transcrypt.py """ - run_tests() + test_configuration_grid() diff --git a/transcrypt b/transcrypt index b41b776..f639708 100755 --- a/transcrypt +++ b/transcrypt @@ -682,7 +682,7 @@ save_helper_hooks() { fi } -# write the configuration to the repository's git config +# "install" transcrypt by writing the configuration to the repository's git config save_configuration() { save_helper_scripts save_helper_hooks From a71c31fe331e79f4f1e97fda9238c666ea76717f Mon Sep 17 00:00:00 2001 From: joncrall Date: Fri, 6 May 2022 16:36:25 -0400 Subject: [PATCH 07/15] include rekey tests --- tests/test_transcrypt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transcrypt.py b/tests/test_transcrypt.py index 34c8964..3f1028d 100644 --- a/tests/test_transcrypt.py +++ b/tests/test_transcrypt.py @@ -469,7 +469,6 @@ def test_configuration_grid(): self = TestEnvironment(config={'use_pbkdf2': 1}) """ # Test that transcrypt works under a variety of config conditions - basis = { 'cipher': ['aes-256-cbc', 'aes-128-ecb'], 'password': ['correct horse battery staple'], @@ -485,6 +484,7 @@ def test_configuration_grid(): self.setup() self.test_round_trip() self.test_export_gpg() + self.test_rekey() if __name__ == '__main__': From 8e18bbf40caeae71b9a6e881a1df0d88b3e79134 Mon Sep 17 00:00:00 2001 From: joncrall Date: Sat, 7 May 2022 17:31:31 -0400 Subject: [PATCH 08/15] OpenSSL 3 support. Fixed tests --- CHANGELOG.md | 12 +++ tests/test_transcrypt.py | 49 +++++++--- transcrypt | 37 +++++++- bash_helpers.sh => transcrypt_bashlib.sh | 116 ++++++++++++++++++++--- 4 files changed, 182 insertions(+), 32 deletions(-) rename bash_helpers.sh => transcrypt_bashlib.sh (71%) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc9bb1..b95dad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ The format is based on [Keep a Changelog][1], and this project adheres to [1]: https://keepachangelog.com/en/1.0.0/ [2]: https://semver.org/spec/v2.0.0.html + +## [3.0.0] - [Unreleased] + +### Added +- Add support for pbkdf2 +- Add support for user specified digest +- Add support for new configured salt method +- Add .transcrypt versioned directory +- Support for OpenSSL 3.x +- Add support for development editable install + + ## [Unreleased] ### Added diff --git a/tests/test_transcrypt.py b/tests/test_transcrypt.py index 3f1028d..cae661f 100644 --- a/tests/test_transcrypt.py +++ b/tests/test_transcrypt.py @@ -5,9 +5,15 @@ pip install GitPython """ import ubelt as ub -import base64 -SALTED = base64.b64encode(b'Salted').decode('utf8') +# U2FsdGVkX18= +# 53616c7465645f5f +__salt_notes__ = ''' + import base64 + salted_bytes = b'Salted' + base64.b64encode(salted_bytes) +''' +SALTED_B64 = 'U2FsdGVk' class Transcrypt(ub.NiceRepr): @@ -18,16 +24,17 @@ class Transcrypt(ub.NiceRepr): >>> import sys, ubelt >>> sys.path.append(ubelt.expandpath('~/code/transcrypt/tests')) >>> from test_transcrypt import * # NOQA - >>> sandbox = DemoSandbox(verbose=0).setup() + >>> sandbox = DemoSandbox(verbose=1, dpath='special:cache').setup() >>> config = {'digest': 'sha256', >>> 'use_pbkdf2': '1', >>> 'config_salt': '665896be121e1a0a4a7b18f01780061', >>> 'salt_method': 'configured'} >>> self = Transcrypt(sandbox.repo_dpath, - >>> config=config, env=sandbox.env, verbose=0) + >>> config=config, env=sandbox.env, verbose=1) >>> print(self.version()) >>> self.config['password'] = 'chbs' >>> self.login() + >>> sandbox.git.commit('-am', 'new salt config') >>> print(self.display()) >>> secret_fpath1 = self.dpath / 'safe/secret1.txt' >>> secret_fpath2 = self.dpath / 'safe/secret2.txt' @@ -36,7 +43,7 @@ class Transcrypt(ub.NiceRepr): >>> secret_fpath2.write_text('secret message 2') >>> secret_fpath3.write_text('secret message 3') >>> sandbox.git.add(secret_fpath1, secret_fpath2, secret_fpath3) - >>> sandbox.git.commit('-am add secret messages') + >>> sandbox.git.commit('-am', 'add secret messages') >>> encrypted_paths = self.list() >>> assert len(encrypted_paths) == 3 >>> assert self.show_raw(secret_fpath1) == 'U2FsdGVkX18147KP5UmqOFywveuOGf4hCwrWpfJDp3Ah0HHbFPEGdJE0kM4npWzI' @@ -209,10 +216,14 @@ class DemoSandbox(ub.NiceRepr): """ def __init__(self, dpath=None, verbose=0): if dpath is None: + dpath = 'special:temp' + + if dpath == 'special:temp': import tempfile self._tmpdir = tempfile.TemporaryDirectory() dpath = self._tmpdir.name - # dpath = ub.Path.appdir('transcrypt/tests/test_env') + elif dpath == 'special:cache': + dpath = ub.Path.appdir('transcrypt/tests/test_env') self.env = {} self.dpath = ub.Path(dpath) self.gpg_store = None @@ -302,14 +313,18 @@ class TestCases: Unit tests to be applied to different transcrypt configurations """ - def __init__(self, config, verbose=0): + def __init__(self, config=None, dpath=None, verbose=0): + if config is None: + config = Transcrypt.default_config + config['password'] = '12345' self.config = config self.verbose = verbose self.sandbox = None self.tc = None + self.dpath = dpath def setup(self): - self.sandbox = DemoSandbox(verbose=self.verbose) + self.sandbox = DemoSandbox(dpath=self.dpath, verbose=self.verbose) self.sandbox.setup() self.tc = Transcrypt( dpath=self.sandbox.repo_dpath, @@ -328,9 +343,9 @@ def test_round_trip(self): secret_fpath = self.sandbox.secret_fpath ciphertext = self.tc.show_raw(secret_fpath) plaintext = secret_fpath.read_text() - assert ciphertext.startswith(SALTED) + assert ciphertext.startswith(SALTED_B64) assert plaintext.startswith('secret content') - assert not plaintext.startswith(SALTED) + assert not plaintext.startswith(SALTED_B64) self.tc.logout() logged_out_text = secret_fpath.read_text().rstrip() @@ -455,9 +470,9 @@ def test_configuration_grid(): >>> import sys, ubelt >>> sys.path.append(ubelt.expandpath('~/code/transcrypt/tests')) >>> from test_transcrypt import * # NOQA - >>> self = DemoSandbox() + >>> self = TestCases() >>> self.setup() - >>> self.tc._manual_hack_info() + >>> self.sandbox._manual_hack_info() >>> self.test_round_trip() >>> self.test_export_gpg() @@ -478,10 +493,16 @@ def test_configuration_grid(): 'config_salt': ['', 'mylittlecustomsalt'], } test_grid = list(ub.named_product(basis)) - for params in ub.ProgIter(test_grid, desc='test configs'): + dpath = 'special:temp' + dpath = 'special:cache' + for params in ub.ProgIter(test_grid, desc='test configs', freq=1): config = params.copy() - self = TestCases(config=config) + self = TestCases(config=config, dpath=dpath) self.setup() + if 0: + # Manual debug + self.sandbox._manual_hack_info() + self.test_round_trip() self.test_export_gpg() self.test_rekey() diff --git a/transcrypt b/transcrypt index f639708..757aa34 100755 --- a/transcrypt +++ b/transcrypt @@ -21,7 +21,7 @@ set -euo pipefail ##### CONSTANTS # the release version of this script -readonly VERSION='2.2.0-pre' +readonly VERSION='3.0.0-pre' # the default cipher to utilize readonly DEFAULT_CIPHER='aes-256-cbc' @@ -296,6 +296,33 @@ die() { exit "$st" } + +_openssl_encrypt() +{ + # Test the openssl version + openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1) + if [ "$openssl_major_version" -ge "3" ]; then + # OpenSSL 3.x + # In 3.x openssl disabled output of the salt prefix, which we need for determinism. + # To reenable the prefix we emit the raw prefix bytes, encrypt in raw bytes, and then + # send that entire stream to be base64 encoded + (printf "Salted__" && printf "%s" "$salt" | xxd -r -p && \ + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + ) | base64 + else + # OpenSSL 1.x + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + fi +} + +_openssl_decrypt() +{ + # Exepcts that the following variables are set: + # password, openssl_path, cipher, digest, pbkdf2_args + # This works the same across openssl versions + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a +} + # 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 @@ -331,9 +358,9 @@ git_clean() { cat "$tempfile" else _load_vars_for_encryption - # NOTE: salt must be 16 bytes, its openssl standard + # NOTE: the openssl standard for salt is 16 hex bytes. salt=$("$openssl_path" dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16) - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + _openssl_encrypt fi } @@ -344,7 +371,7 @@ git_smudge() { trap 'rm -f "$tempfile"' EXIT #_load_transcrypt_config_vars _load_vars_for_encryption - tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a 2>/dev/null || cat "$tempfile" + tee "$tempfile" | _openssl_decrypt 2>/dev/null || cat "$tempfile" } git_textconv() { @@ -357,7 +384,7 @@ git_textconv() { return fi _load_transcrypt_config_vars - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a -in "$filename" 2>/dev/null || cat "$filename" + _openssl_decrypt -in "$filename" 2>/dev/null || cat "$filename" } # shellcheck disable=SC2005,SC2002,SC2181 diff --git a/bash_helpers.sh b/transcrypt_bashlib.sh similarity index 71% rename from bash_helpers.sh rename to transcrypt_bashlib.sh index f3c3252..2b2e6cd 100644 --- a/bash_helpers.sh +++ b/transcrypt_bashlib.sh @@ -1,4 +1,71 @@ #!/usr/bin/env bash +__doc__=' +This contains the standalone heredoc versions of transcrypt library functions. +These are not used in the main executable itself. Instead they are ported from +here to there and stripped of extranious information. + +This makes it easier to unit test the individual bash components of the system +while still providing a fast and reasonably optimized runtime. +' + + +# shellcheck disable=SC2154 +_openssl_encrypt() +{ + __doc__=' + Example: + source ~/code/transcrypt/transcrypt_bashlib.sh + pbkdf2_args=("-pbkdf2") + salt=deadbeafbad00000 + digest=sha256 + password=12345 + openssl_path=openssl + cipher=aes-256-cbc + tempfile=$(mktemp) + echo "secret" > $tempfile + _openssl_encrypt + ' + # Exepcts that the following variables are set: + # password, openssl_path, cipher, digest, salt, pbkdf2_args, tempfile + + # Test the openssl version + openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1) + if [ "$openssl_major_version" -ge "3" ]; then + # OpenSSL 3.x + # In 3.x openssl disabled output of the salt prefix, which we need for determinism. + # To reenable the prefix we emit the raw prefix bytes, encrypt in raw bytes, and then + # send that entire stream to be base64 encoded + (printf "Salted__" && printf "%s" "$salt" | xxd -r -p && \ + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + ) | base64 + else + # OpenSSL 1.x + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + fi +} + +# shellcheck disable=SC2154 +_openssl_decrypt() +{ + __doc__=' + Example: + source ~/code/transcrypt/transcrypt_bashlib.sh + pbkdf2_args=("-pbkdf2") + digest=sha256 + password=12345 + openssl_path=openssl + cipher=aes-256-cbc + echo "U2FsdGVkX1/erb6vutAAADPXEjWJ3l4MEpSGTj5qC/w=" | _openssl_decrypt + tempfile=$(mktemp) + echo "U2FsdGVkX1/erb6vutAAADPXEjWJ3l4MEpSGTj5qC/w=" > $tempfile + _openssl_decrypt -in $tempfile + ' + # Exepcts that the following variables are set: + # password, openssl_path, cipher, digest, pbkdf2_args + # This works the same across openssl versions + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a "$@" +} + _is_contained_str(){ __doc__=' @@ -56,18 +123,6 @@ _is_contained_arr(){ return 1 } -_benchmark_methods(){ - arg="sha512" - source ~/code/transcrypt/bash_helpers.sh - time (openssl list -digest-commands | tr -s ' ' '\n' | grep -Fx "$arg") - echo $? - time _is_contained_str "$arg" "$(openssl list -digest-commands)" - echo $? - time (readarray -t available <<< "$(openssl list -digest-commands | tr -s ' ' '\n')" && _is_contained_arr "$arg" "${available[@]}") - echo $? - #bash_array_repr "${available[@]}" -} - joinby(){ __doc__=' @@ -124,7 +179,7 @@ _validate_variable_arr(){ echo "$message" else die 1 "$message" - fi + s fi fi } @@ -238,3 +293,38 @@ _check_config_poc(){ git config --file "${VERSIONED_TC_CONFIG}" transcrypt.salt-method "auto" git config --file "${VERSIONED_TC_CONFIG}" transcrypt.extra-salt "${extra_salt}" } + + +# 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" +} + +_benchmark_methods(){ + arg="sha512" + source ~/code/transcrypt/bash_helpers.sh + time (openssl list -digest-commands | tr -s ' ' '\n' | grep -Fx "$arg") + echo $? + time _is_contained_str "$arg" "$(openssl list -digest-commands)" + echo $? + # Odd vim syntax issue? + # ~/.pyenv/versions/3.9.9/share/vim/vim82/syntax/sh.vim + time (readarray -t available <<< "$(openssl list -digest-commands | tr -s ' ' '\n')" && _is_contained_arr "$arg" "${available[@]}") + echo $? + #bash_array_repr "${available[@]}" +} From 0e88672a9cc27f7b9eb43666c09f7fda9987b9da Mon Sep 17 00:00:00 2001 From: joncrall Date: Sat, 7 May 2022 17:44:21 -0400 Subject: [PATCH 09/15] environ for TRANSCRYPT_EDITABLE_INSTALL --- tests/test_transcrypt.py | 5 +- transcrypt | 86 ++++++------ transcrypt_bashlib.sh | 273 ++++++++++++++++++++++++++++----------- 3 files changed, 239 insertions(+), 125 deletions(-) diff --git a/tests/test_transcrypt.py b/tests/test_transcrypt.py index cae661f..4756605 100644 --- a/tests/test_transcrypt.py +++ b/tests/test_transcrypt.py @@ -6,8 +6,6 @@ """ import ubelt as ub -# U2FsdGVkX18= -# 53616c7465645f5f __salt_notes__ = ''' import base64 salted_bytes = b'Salted' @@ -393,7 +391,6 @@ def test_export_gpg(self): assert plaintext.startswith('secret content') def test_rekey(self): - # TODO new_config = { 'cipher': 'aes-256-cbc', 'password': '12345', @@ -464,7 +461,7 @@ def test_configured_salt_changes_on_rekey(): def test_configuration_grid(): """ CommandLine: - xdoctest -m /home/joncrall/code/transcrypt/tests/test_transcrypt.py run_tests + xdoctest -m tests/test_transcrypt.py test_configuration_grid Example: >>> import sys, ubelt diff --git a/transcrypt b/transcrypt index 757aa34..136acdd 100755 --- a/transcrypt +++ b/transcrypt @@ -1,11 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -# TODO: -# Remove "salt_method" -# Just have a setting where the password is allowed to be in the salt and a -# setting where the user does have a configured salt? - # # transcrypt - https://github.com/elasticdog/transcrypt # @@ -37,7 +32,7 @@ readonly VALID_SALT_METHODS="password configured" readonly VERSIONED_CONFIG_BLOCKLIST="transcrypt.password transcrypt.openssl-path" # Set to 1 to enable a development editable installation -readonly EDITABLE_INSTALL=1 +readonly EDITABLE_INSTALL=${TRANSCRYPT_EDITABLE_INSTALL:=0} ##### FUNCTIONS @@ -69,6 +64,46 @@ realpath() { fi } + +_openssl_encrypt() +{ + # Test the openssl version + openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1) + if [ "$openssl_major_version" -ge "3" ]; then + # OpenSSL 3.x + # In 3.x openssl disabled output of the salt prefix, which we need for determinism. + # To reenable the prefix we emit the raw prefix bytes, encrypt in raw bytes, and then + # send that entire stream to be base64 encoded + (printf "Salted__" && printf "%s" "$salt" | xxd -r -p && \ + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + ) | base64 + else + # OpenSSL 1.x + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + fi +} + +_openssl_decrypt() +{ + # Exepcts that the following variables are set: + # password, openssl_path, cipher, digest, pbkdf2_args + # This works the same across openssl versions + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a +} + + +# compatible openssl list command +_openssl_list(){ + arg=$1 + if "${openssl_path} list-$arg" &>/dev/null; then + # OpenSSL < v1.1.0 + "${openssl_path}" "list-$arg" + else + # OpenSSL >= v1.1.0 + "${openssl_path}" "list" "-$arg" + fi +} + # sets a bash global variable by name _set_global(){ key=$1 @@ -119,18 +154,6 @@ _get_user_input() { done } -# compatible openssl list command -_openssl_list(){ - arg=$1 - if "${openssl_path} list-$arg" &>/dev/null; then - # OpenSSL < v1.1.0 - "${openssl_path}" "list-$arg" - else - # OpenSSL >= v1.1.0 - "${openssl_path}" "list" "-$arg" - fi -} - # Check if the first arg is contained in the space separated second arg _is_contained_str(){ arg=$1 @@ -296,33 +319,6 @@ die() { exit "$st" } - -_openssl_encrypt() -{ - # Test the openssl version - openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1) - if [ "$openssl_major_version" -ge "3" ]; then - # OpenSSL 3.x - # In 3.x openssl disabled output of the salt prefix, which we need for determinism. - # To reenable the prefix we emit the raw prefix bytes, encrypt in raw bytes, and then - # send that entire stream to be base64 encoded - (printf "Salted__" && printf "%s" "$salt" | xxd -r -p && \ - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" - ) | base64 - else - # OpenSSL 1.x - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" - fi -} - -_openssl_decrypt() -{ - # Exepcts that the following variables are set: - # password, openssl_path, cipher, digest, pbkdf2_args - # This works the same across openssl versions - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a -} - # 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 diff --git a/transcrypt_bashlib.sh b/transcrypt_bashlib.sh index 2b2e6cd..0d588ac 100644 --- a/transcrypt_bashlib.sh +++ b/transcrypt_bashlib.sh @@ -8,6 +8,79 @@ This makes it easier to unit test the individual bash components of the system while still providing a fast and reasonably optimized runtime. ' +# 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" +} + +# 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 +} + +joinby(){ + __doc__=' + A function that works similar to a Python join + + Args: + SEP: the separator + *ARR: elements of the strings to join + + Usage: + source $HOME/local/init/utils.sh + ARR=("foo" "bar" "baz") + RESULT=$(joinby / "${ARR[@]}") + echo "RESULT = $RESULT" + + RESULT = foo/bar/baz + + References: + https://stackoverflow.com/questions/1527049/how-can-i-join-elements-of-an-array-in-bash + ' + _handle_help "$@" || return 0 + local d=${1-} f=${2-} + if shift 2; then + printf %s "$f" "${@/#/$d}" + fi +} # shellcheck disable=SC2154 _openssl_encrypt() @@ -66,6 +139,25 @@ _openssl_decrypt() ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a "$@" } +_openssl_list(){ + # Args: the openssl commands to list + __doc__=' + source ~/code/transcrypt/bash_helpers.sh + arg=digest-commands + _openssl_list digest-commands + _openssl_list cipher-commands + ' + openssl_path=openssl + arg=$1 + if "${openssl_path} list-$arg" &>/dev/null; then + # OpenSSL < v1.1.0 + "${openssl_path}" "list-$arg" + else + # OpenSSL >= v1.1.0 + "${openssl_path}" "list" "-$arg" + fi +} + _is_contained_str(){ __doc__=' @@ -123,33 +215,6 @@ _is_contained_arr(){ return 1 } - -joinby(){ - __doc__=' - A function that works similar to a Python join - - Args: - SEP: the separator - *ARR: elements of the strings to join - - Usage: - source $HOME/local/init/utils.sh - ARR=("foo" "bar" "baz") - RESULT=$(joinby / "${ARR[@]}") - echo "RESULT = $RESULT" - - RESULT = foo/bar/baz - - References: - https://stackoverflow.com/questions/1527049/how-can-i-join-elements-of-an-array-in-bash - ' - _handle_help "$@" || return 0 - local d=${1-} f=${2-} - if shift 2; then - printf %s "$f" "${@/#/$d}" - fi -} - _set_global(){ # sets a bash global variable by name key=$1 @@ -179,7 +244,7 @@ _validate_variable_arr(){ echo "$message" else die 1 "$message" - s fi + fi fi } @@ -256,75 +321,131 @@ _get_user_input2() { done } -_openssl_list(){ - # Args: the openssl commands to list - __doc__=' - source ~/code/transcrypt/bash_helpers.sh - arg=digest-commands - _openssl_list digest-commands - _openssl_list cipher-commands - ' - openssl_path=openssl - arg=$1 - if "${openssl_path} list-$arg" &>/dev/null; then - # OpenSSL < v1.1.0 - "${openssl_path}" "list-$arg" - else - # OpenSSL >= v1.1.0 - "${openssl_path}" "list" "-$arg" - fi + +# Helper to prompt the user, store a response, and validate the result +_get_user_input() { + local varname=$1 + local default=$2 + local validate_fn=$3 + local prompt=$4 + + while [[ ! ${!varname} ]]; do + local answer= + if [[ $interactive ]]; then + printf '%s' "$prompt" + read -r answer + fi + # use the default value if the user gave no answer; otherwise call the + # validate function, which should set the varname to empty if it is + # invalid and the user should continue, otherwise it should die. + if [[ ! $answer ]]; then + _set_global "$varname" "$default" + else + _set_global "$varname" "$answer" + ${validate_fn} + fi + done } +# Load a config var from the versioned config # shellcheck disable=SC2155 -_check_config_poc(){ - # Notes on custom config +_load_versioned_config_var(){ + # the current git repository's top-level directory + if [ -z "${VERSIONED_TC_CONFIG+x}" ]; then + readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) + # This is the place where transcrypt can store state that will be + # "versioned" (i.e. checked into the repo) + readonly VERSIONED_TC_DIR="${REPO}/.transcrypt" + readonly VERSIONED_TC_CONFIG="${VERSIONED_TC_DIR}/config" + fi + local key=$1 + # Test for blocked variables that should not go into a plaintext config file + if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then + warn "Cannot use ${key} in versioned the transcrypt config" + return 1 + fi # https://unix.stackexchange.com/questions/175648/use-config-file-for-my-shell-script - mkdir -p "${VERSIONED_CONFIG_DPATH}" - touch "${VERSIONED_TC_CONFIG}" - git config -f "$VERSIONED_TC_CONFIG" --get transcrypt.cipher - git config -f "$VERSIONED_TC_CONFIG" --get transcrypt.rotating.salt - - # POC for using git to store cross-checkout configs - extra_salt=$(openssl rand -hex 32) - git config --file "${VERSIONED_TC_CONFIG}" transcrypt.cipher "aes-256-cbc" - git config --file "${VERSIONED_TC_CONFIG}" transcrypt.use-pbkdf2 "true" --type=bool - git config --file "${VERSIONED_TC_CONFIG}" transcrypt.digest "SHA512" - git config --file "${VERSIONED_TC_CONFIG}" transcrypt.salt-method "auto" - git config --file "${VERSIONED_TC_CONFIG}" transcrypt.extra-salt "${extra_salt}" + git config --file "${VERSIONED_TC_CONFIG}" --get "${key}" || true } +# Write a config var to the versioned config +_set_versioned_config_var(){ + local key=$1 + local val=$2 + # Test for blocked variables that should not go into a plaintext config file + if _is_contained_str "${key}" "${VERSIONED_CONFIG_BLOCKLIST}"; then + warn "Cannot use ${key} in versioned the transcrypt config" + return 1 + fi + mkdir -p "${VERSIONED_TC_DIR}" + git config --file "${VERSIONED_TC_CONFIG}" "${key}" "${val}" +} -# print a message to stderr -warn() { - local fmt="$1" - shift - # shellcheck disable=SC2059 - printf "transcrypt: $fmt\n" "$@" >&2 +# +_load_config_var(){ + # First try loading from the local checkout-independent .git/config file + # If that doesn't work, then look in the .transcrypt/config file + # (which is expected to be stored in plaintext and checked into the repo) + # Certain values will be blocked from being placed here (like the password) + local key=$1 + git config --get --local "${key}" + if [[ "$?" != "0" ]]; then + _load_versioned_config_var "${key}" + fi } -# 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 +# shellcheck disable=SC2155 +_load_transcrypt_config_vars(){ + # Populate bash vars with our config + cipher=$(git config --get --local transcrypt.cipher) + digest=$(git config --get --local transcrypt.digest) + use_pbkdf2=$(git config --get --local transcrypt.use-pbkdf2) + salt_method=$(git config --get --local transcrypt.salt-method) + + password=$(git config --get --local transcrypt.password) + openssl_path=$(git config --get --local transcrypt.openssl-path) + + if [[ "$salt_method" == "configured" ]]; then + config_salt=$(_load_versioned_config_var "transcrypt.config-salt") + else + config_salt="" + fi +} + +_load_vars_for_encryption(){ + # Helper to populate variables needed to call openssl encryption + _load_transcrypt_config_vars + + if [[ "$use_pbkdf2" == "1" ]]; then + pbkdf2_args=('-pbkdf2') + else + pbkdf2_args=() + fi + + if [[ "$salt_method" == "password" ]]; then + extra_salt=$password + elif [[ "$salt_method" == "configured" ]]; then + extra_salt=$config_salt + else + die "unknown salt method" + fi + + if [[ "$extra_salt" == "" ]]; then + die "Extra salt is not set" fi - warn "$@" - exit "$st" } _benchmark_methods(){ + # Development helepr to determine which way of checking if we have a available digest / cipher is fastest arg="sha512" source ~/code/transcrypt/bash_helpers.sh time (openssl list -digest-commands | tr -s ' ' '\n' | grep -Fx "$arg") echo $? time _is_contained_str "$arg" "$(openssl list -digest-commands)" echo $? - # Odd vim syntax issue? - # ~/.pyenv/versions/3.9.9/share/vim/vim82/syntax/sh.vim time (readarray -t available <<< "$(openssl list -digest-commands | tr -s ' ' '\n')" && _is_contained_arr "$arg" "${available[@]}") echo $? #bash_array_repr "${available[@]}" } + From 8632986a8e9e78d72a630ea161719e39988a3f3e Mon Sep 17 00:00:00 2001 From: joncrall Date: Sat, 7 May 2022 17:52:49 -0400 Subject: [PATCH 10/15] update contrib --- contrib/bash/transcrypt | 16 +++++++++++++++- contrib/packaging/pacman/PKGBUILD | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/contrib/bash/transcrypt b/contrib/bash/transcrypt index 07dafa0..eb9a17b 100644 --- a/contrib/bash/transcrypt +++ b/contrib/bash/transcrypt @@ -26,13 +26,27 @@ _transcrypt() { case "${prev}" in -c | --cipher) - local ciphers=$(openssl list-cipher-commands) + local ciphers=$(openssl list-cipher-commands || openssl list -cipher-commands &2>/dev/null) COMPREPLY=( $(compgen -W "${ciphers}" -- ${cur}) ) return 0 ;; -p | --password) return 0 ;; + -md | --digest) + local ciphers=$(openssl list-digest-commands || openssl list -digest-commands &2>/dev/null) + COMPREPLY=( $(compgen -W "${ciphers}" -- ${cur}) ) + return 0 + ;; + --use-pbkdf2) + return 0 + ;; + -sm | --salt-method) + return 0 + ;; + -cs | --config-salt) + return 0 + ;; -s | --show-raw) _files_and_dirs return 0 diff --git a/contrib/packaging/pacman/PKGBUILD b/contrib/packaging/pacman/PKGBUILD index beb4759..553eabf 100644 --- a/contrib/packaging/pacman/PKGBUILD +++ b/contrib/packaging/pacman/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Aaron Bull Schaefer pkgname=transcrypt -pkgver=2.1.0 +pkgver=3.0.0 pkgrel=1 pkgdesc='A script to configure transparent encryption of files within a Git repository' arch=('any') From db8fbae27c4d31fb6da5a5a163464a06fa767f7b Mon Sep 17 00:00:00 2001 From: joncrall Date: Sat, 7 May 2022 17:55:15 -0400 Subject: [PATCH 11/15] update contrib --- contrib/zsh/_transcrypt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contrib/zsh/_transcrypt b/contrib/zsh/_transcrypt index 523df12..63daa0f 100644 --- a/contrib/zsh/_transcrypt +++ b/contrib/zsh/_transcrypt @@ -11,6 +11,10 @@ _transcrypt() { '(- 1 *)'{-v,--version}'[print version]' \ '(- 1 *)'{-h,--help}'[view help message]' \ '(-c --cipher -d --display -f --flush-credentials -u --uninstall)'{-c,--cipher=}'[specify encryption cipher]:cipher:->cipher' \ + '(-md --digest -d --display -f --flush-credentials -u --uninstall)'{-md,--digest=}'[specify encryption digest]:digest' \ + '(-sm --salt-method -d --display -f --flush-credentials -u --uninstall)'{-md,--digest=}'[specify salt-method]:salt-method' \ + '(-cs --config-salt -d --display -f --flush-credentials -u --uninstall)'{-md,--digest=}'[specify config-salt]:config-salt' \ + '(-pbkdf2 --use-pbkdf2 -d --display -f --flush-credentials -u --uninstall)'{-md,--digest=}'[specify use-pbkdf2]:use-pbkdf2' \ '(-p --password -d --display -f --flush-credentials -u --uninstall)'{-p,--password=}'[specify encryption password]:password:' \ '(-y --yes)'{-y,--yes}'[assume yes and accept defaults]' \ '(-d --display -p --password -c --cipher -r --rekey -u --uninstall)'{-d,--display}'[display current credentials]' \ From c94f89ac9c47fcb2e128f2ca302b3648c268bcb8 Mon Sep 17 00:00:00 2001 From: joncrall Date: Sat, 7 May 2022 22:49:00 -0400 Subject: [PATCH 12/15] Use shfmt --- transcrypt | 104 ++++++++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/transcrypt b/transcrypt index 136acdd..995436c 100755 --- a/transcrypt +++ b/transcrypt @@ -64,36 +64,33 @@ realpath() { fi } - -_openssl_encrypt() -{ - # Test the openssl version - openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1) - if [ "$openssl_major_version" -ge "3" ]; then - # OpenSSL 3.x - # In 3.x openssl disabled output of the salt prefix, which we need for determinism. - # To reenable the prefix we emit the raw prefix bytes, encrypt in raw bytes, and then - # send that entire stream to be base64 encoded - (printf "Salted__" && printf "%s" "$salt" | xxd -r -p && \ - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" - ) | base64 - else - # OpenSSL 1.x - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" - fi +_openssl_encrypt() { + # Test the openssl version + openssl_major_version=$($openssl_path version | cut -d' ' -f2 | cut -d'.' -f1) + if [ "$openssl_major_version" -ge "3" ]; then + # OpenSSL 3.x + # In 3.x openssl disabled output of the salt prefix, which we need for determinism. + # To reenable the prefix we emit the raw prefix bytes, encrypt in raw bytes, and then + # send that entire stream to be base64 encoded + ( + printf "Salted__" && printf "%s" "$salt" | xxd -r -p && + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + ) | base64 + else + # OpenSSL 1.x + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -in "$tempfile" + fi } -_openssl_decrypt() -{ - # Exepcts that the following variables are set: - # password, openssl_path, cipher, digest, pbkdf2_args - # This works the same across openssl versions - ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a +_openssl_decrypt() { + # Exepcts that the following variables are set: + # password, openssl_path, cipher, digest, pbkdf2_args + # This works the same across openssl versions + ENC_PASS=$password "$openssl_path" enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a } - # compatible openssl list command -_openssl_list(){ +_openssl_list() { arg=$1 if "${openssl_path} list-$arg" &>/dev/null; then # OpenSSL < v1.1.0 @@ -105,16 +102,15 @@ _openssl_list(){ } # sets a bash global variable by name -_set_global(){ +_set_global() { key=$1 val=$2 printf -v "$key" '%s' "$val" } - # Checks if the target variable is in the set of valid values. If it is not, it # unsets the target variable, then if not in interactive mode it calls die. -_validate_variable_str(){ +_validate_variable_str() { local varname=$1 local valid_values=$2 local varval=${!varname} @@ -155,16 +151,15 @@ _get_user_input() { } # Check if the first arg is contained in the space separated second arg -_is_contained_str(){ +_is_contained_str() { arg=$1 values=$2 - echo "$values" | tr -s ' ' '\n' | grep -Fx "$arg" &>/dev/null + echo "$values" | tr -s ' ' '\n' | grep -Fx "$arg" &>/dev/null } - -# Load a config var from the versioned config +# Load a config var from the versioned config # shellcheck disable=SC2155 -_load_versioned_config_var(){ +_load_versioned_config_var() { # the current git repository's top-level directory if [ -z "${VERSIONED_TC_CONFIG+x}" ]; then readonly REPO=$(git rev-parse --show-toplevel 2>/dev/null) @@ -182,8 +177,8 @@ _load_versioned_config_var(){ git config --file "${VERSIONED_TC_CONFIG}" --get "${key}" || true } -# Write a config var to the versioned config -_set_versioned_config_var(){ +# Write a config var to the versioned config +_set_versioned_config_var() { local key=$1 local val=$2 # Test for blocked variables that should not go into a plaintext config file @@ -195,8 +190,8 @@ _set_versioned_config_var(){ git config --file "${VERSIONED_TC_CONFIG}" "${key}" "${val}" } -# -_load_config_var(){ +# +_load_config_var() { # First try loading from the local checkout-independent .git/config file # If that doesn't work, then look in the .transcrypt/config file # (which is expected to be stored in plaintext and checked into the repo) @@ -209,7 +204,7 @@ _load_config_var(){ } # shellcheck disable=SC2155 -_load_transcrypt_config_vars(){ +_load_transcrypt_config_vars() { # Populate bash vars with our config cipher=$(git config --get --local transcrypt.cipher) digest=$(git config --get --local transcrypt.digest) @@ -226,8 +221,7 @@ _load_transcrypt_config_vars(){ fi } - -_load_vars_for_encryption(){ +_load_vars_for_encryption() { # Helper to populate variables needed to call openssl encryption _load_transcrypt_config_vars @@ -245,12 +239,11 @@ _load_vars_for_encryption(){ die "unknown salt method" fi - if [[ "$extra_salt" == "" ]]; then + if [[ "$extra_salt" == "" ]]; then die "Extra salt is not set" fi } - # establish repository metadata and directory handling # shellcheck disable=SC2155 gather_repo_metadata() { @@ -340,7 +333,7 @@ git_clean() { fi # cache STDIN to test if it's already encrypted # First, create the tempfile, then - # set a trap to remove the tempfile when we exit or if anything goes wrong + # set a trap to remove the tempfile when we exit or if anything goes wrong # finally write the stdin of this script to the tempfile tempfile=$(mktemp 2>/dev/null || mktemp -t tmp) trap 'rm -f "$tempfile"' EXIT @@ -356,7 +349,7 @@ git_clean() { _load_vars_for_encryption # NOTE: the openssl standard for salt is 16 hex bytes. salt=$("$openssl_path" dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16) - _openssl_encrypt + _openssl_encrypt fi } @@ -371,9 +364,9 @@ git_smudge() { } git_textconv() { - # The textconv script allows users to see git diffs in plaintext. + # The textconv script allows users to see git diffs in plaintext. # It does this by decrypting the encrypted git globs into plain text before - # passing them to the diff command. + # passing them to the diff command. filename=$1 # ignore empty files if [[ ! -s $filename ]]; then @@ -546,8 +539,7 @@ validate_use_pbkdf2() { _validate_variable_str "use_pbkdf2" "0 1" } - -validate_salt_method(){ +validate_salt_method() { _validate_variable_str "salt_method" "$VALID_SALT_METHODS" } @@ -558,7 +550,6 @@ get_digest() { _get_user_input digest "$DEFAULT_DIGEST" "validate_digest" "$prompt" } - # ensure we have a cipher to encrypt with get_cipher() { local prompt @@ -572,7 +563,6 @@ get_use_pbkdf2() { _get_user_input use_pbkdf2 "$DEFAULT_USE_PBKDF2" "validate_use_pbkdf2" "$prompt" } - get_salt_method() { local prompt prompt=$(printf 'Compute salt using which method? [%s] ' "$DEFAULT_SALT_METHOD") @@ -604,7 +594,6 @@ get_password() { done } - # confirm the transcrypt configuration confirm_configuration() { local answer= @@ -735,12 +724,12 @@ save_configuration() { # The user might not care about cross machine transparency git config transcrypt.config-salt "$config_salt" _set_versioned_config_var "transcrypt.config-salt" "$config_salt" - if ! git ls-files --error-unmatch "$RELATIVE_VERSIONED_TC_CONFIG" > /dev/null 2>&1; then + if ! git ls-files --error-unmatch "$RELATIVE_VERSIONED_TC_CONFIG" >/dev/null 2>&1; then git add "${RELATIVE_VERSIONED_TC_CONFIG}" printf "*** The contents of %s were configured. ***\n" "${RELATIVE_VERSIONED_TC_CONFIG}" printf '*** COMMIT THIS CHANGE RIGHT AWAY! ***\n\n' fi - if ! git diff --exit-code "$RELATIVE_VERSIONED_TC_CONFIG" > /dev/null 2>&1; then + if ! git diff --exit-code "$RELATIVE_VERSIONED_TC_CONFIG" >/dev/null 2>&1; then git add "${RELATIVE_VERSIONED_TC_CONFIG}" printf "*** The contents of %s were updated. ***\n" "${RELATIVE_VERSIONED_TC_CONFIG}" printf '*** COMMIT THIS CHANGE RIGHT AWAY! ***\n\n' @@ -766,8 +755,7 @@ save_configuration() { 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_git_configuration(){ +_display_git_configuration() { printf '\nRepository metadata:\n\n' [[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO" printf ' GIT_DIR: %s\n' "$GIT_DIR" @@ -775,7 +763,7 @@ _display_git_configuration(){ } # Show the config of the current runtime -_display_runtime_configuration(){ +_display_runtime_configuration() { printf ' DIGEST: %s\n' "$digest" printf ' USE_PBKDF2: %s\n' "$use_pbkdf2" printf ' SALT_METHOD: %s\n' "$salt_method" @@ -1092,7 +1080,7 @@ export_gpg() { 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 >"${CRYPT_DIR}/${gpg_recipient}.asc" printf 'password=%s\ncipher=%s\ndigest=%s\nuse_pbkdf2=%s\nsalt_method=%s\nconfig_salt=%s\n\n' \ - "$password" "$cipher" "$digest" "$use_pbkdf2" "$salt_method" "$config_salt" | \ + "$password" "$cipher" "$digest" "$use_pbkdf2" "$salt_method" "$config_salt" | $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" } @@ -1511,7 +1499,7 @@ if [[ "$salt_method" == "configured" ]]; then # If the user didnt specify explicitly, try to load the config salt if [[ "$config_salt" == "" ]]; then config_salt=$(_load_versioned_config_var "transcrypt.config-salt") - # If we have not configured the config_salt (or we need to rekey), + # If we have not configured the config_salt (or we need to rekey), # then generate new random salt if [[ "$config_salt" == "" ]] || [[ $rekey ]]; then config_salt=$(openssl rand -hex 32) From 686f62fd9c467f641e263a8a31e5dd0736ada788 Mon Sep 17 00:00:00 2001 From: joncrall Date: Mon, 9 May 2022 19:38:24 -0400 Subject: [PATCH 13/15] Add option for trace and error messages when something goes wrong with config reading --- transcrypt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/transcrypt b/transcrypt index 995436c..bebba64 100755 --- a/transcrypt +++ b/transcrypt @@ -1,6 +1,10 @@ #!/usr/bin/env bash set -euo pipefail +if [[ "${TRANSCRYPT_TRACE+x}" != "" ]]; then + set -x +fi + # # transcrypt - https://github.com/elasticdog/transcrypt # @@ -206,13 +210,13 @@ _load_config_var() { # shellcheck disable=SC2155 _load_transcrypt_config_vars() { # Populate bash vars with our config - cipher=$(git config --get --local transcrypt.cipher) - digest=$(git config --get --local transcrypt.digest) - use_pbkdf2=$(git config --get --local transcrypt.use-pbkdf2) - salt_method=$(git config --get --local transcrypt.salt-method) + cipher=$(git config --get --local transcrypt.cipher) || (echo "failed to load transcrypt.cipher" && false) + digest=$(git config --get --local transcrypt.digest) || (echo "failed to load transcrypt.digest" && false) + use_pbkdf2=$(git config --get --local transcrypt.use-pbkdf2) || (echo "failed to load transcrypt.use-pbkdf2" && false) + salt_method=$(git config --get --local transcrypt.salt-method) || (echo "failed to load transcrypt.salt-method" && false) - password=$(git config --get --local transcrypt.password) - openssl_path=$(git config --get --local transcrypt.openssl-path) + password=$(git config --get --local transcrypt.password) || (echo "failed to load transcrypt.password" && false) + openssl_path=$(git config --get --local transcrypt.openssl-path) || (echo "failed to load transcrypt.openssl-path" && false) if [[ "$salt_method" == "configured" ]]; then config_salt=$(_load_versioned_config_var "transcrypt.config-salt") From 89c5be43adeff74c9d53b267a1c4faa107be5583 Mon Sep 17 00:00:00 2001 From: joncrall Date: Thu, 19 May 2022 18:22:18 -0400 Subject: [PATCH 14/15] Add algorithm writeup --- docs/algorithm.rst | 308 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 docs/algorithm.rst diff --git a/docs/algorithm.rst b/docs/algorithm.rst new file mode 100644 index 0000000..83e0b4b --- /dev/null +++ b/docs/algorithm.rst @@ -0,0 +1,308 @@ +The Transcrypt Algorithm +======================== + +The transcrypt algorithm makes use of the following components: + +* `git _` +* `bash _` +* `openssl _` + +The "clean" and "smudge" git filters implement the core functionality by +encrypting a sensitive file before committing it to the repo history, and +decrypting the file when a local copy of the file is checked out. + +* `filter.crypt.clean` - "transcrypt clean" + +* `filter.crypt.smudge` - "transcrypt smudge" + + +Transcrypt uses openssl for all underlying cryptographic operations. + +From git's perspective, is only tracks the encrypted ciphertext of each file. +Thus is it important that any encryption algorithm used must be deterministic, +otherwise changes in the ciphertext (e.g. due to randomized salt) will cause +git to think the file has changed when it hasn't. + + +Core Algorithms +=============== + +From a high level, lets assume we have a secure process to save / load a +desired configuration. + + +The Encryption Process +---------------------- + +A file is encrypted via the following procedure in the ``filter.crypt.clean`` filter. + +Given a sensitive file specified by ``filename`` + +1. Empty files are ignored + +2. A temporary file is created with the (typically plaintext) contents of ``filename``. + This file only contains user read/write permissions (i.e. 600). + A bash trap is set such that this file is removed when transcrypt exists. + +2. The first 6 bytes of the file are checked. If they are "U2FsdGVk" (which is + indicative of a salted openssl encrypted file, we assume the file is already + encrypted emit it as-is) + +3. Otherwise the transcrypt configuration is loaded (which defines the cipher, + digest, key derivation function, salt, and password), openssl is called to + encrypt the plaintext, and the base64 ciphertext is emitted and passed to git. + +The following is the openssl invocation used in encryption + +.. code:: bash + + ENC_PASS=$password openssl enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" + + +Note: For OpenSSL V3.x, which does not prepend the salt to the ciphertext, we manually prepend the raw salt bytes to the raw openssl output (without ``-a`` for base64 encoding) and then perform base64 encoding of the concatenated text as a secondary task. This makes the output from version 3.x match outputs from the 1.x openssl releases. + + +The Decryption Process +---------------------- + +When a sensitive file is checked out, it is first decrypted before being placed +in the user's working branch via the ``filter.crypt.smudge`` filter. + +1. The ciphertext is passed to the smudge filter via stdin. + +2. The transcrypt configuration is loaded. + +3. The ciphertext is decrypted using openssl and emitted via stdout. If + decryption fails the ciphertext itself is emitted via stdout. + + +The following invocation is used for decryption + +.. code:: bash + + # used to decrypt a file. the cipher, digest, password, and key derivation + # function must be known in advance. the salt is always prepended to the + # file ciphertext, and ready by openssl, so it does not need to be supplied here. + ENC_PASS=$password openssl enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a + + +Configuration +============= + +Loading the configuration is a critical subroutine in the core transcrypt +components. + +In the proposed transcrypt 3.x implementation, the following *bash* variables +are required for encryption and decryption: + +* ``cipher`` +* ``password`` +* ``digest`` +* ``pbkdf2_args`` + + +And additionally, encryption needs the variable: + +* ``salt`` + +Cipher, Password, and Digest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For the first 3 variables ``cipher``, ``password``, and ``digest`` the method +transcrypt uses to store them is straightforward. In the local ``.git/config`` +directory these passwords are stored as checkout-specific git variables stored +in plaintext. + +* ``transcrypt.cipher`` +* ``transcrypt.digest`` +* ``transcrypt.password`` + +Note, that before transcrypt 3.x only cipher and password were configurable. +Legacy behavior of transcrypt is described by assuming digest is MD5. + +The other two variables ``pbkdf2_args`` and ``salt`` are less straight forward. + + +PBKDF2 +~~~~~~ + +The `PBKDF2`_ (Password Based Key Derivation Function v2) adds protection +against brute force attacks by increasing the amount of time it takes to derive +the actual key and iv values used in the encryption / decryption process. + +.. _PBKDF2: https://en.wikipedia.org/wiki/PBKDF2 + +OpenSSL enables ``pbkdf2`` if the ``-pbkdf2`` flag is specified. +To coerce this into a key-value configuration scheme we use the git +configuration variable + +* ``transcrypt.use-pbkdf2`` + +Which can be set to 0 or 1. At configuration load time, depending on the value +in the config transcrypt will set ``pbkdf2_args`` to an empty bash array in the +case where pbkdf2 is disabled, and ``-pbkdf2`` otherwise. This allows us to use +bash array syntax to express both variants as a single openssl command. + +The backwards compatible setting for transcrypt < 3.x is ``--use-pbkdf2=0``. + +See Also: + +PKCS5#5.2 (RFC-2898) +https://datatracker.ietf.org/doc/html/rfc2898#section-5.2 + +Salt +~~~~ + +Lastly, there is ``salt``, which the least straightforward of these options. + +Ideally, when using openssl, a unique and random salt is generated **each +time** the file is encrypted. This prevents an attacker from executing a +known-plaintext attack by pre-computing common password / ciphertext pairs on +small files and being able to determine the user's password if any of the +precomputed ciphertexts exist in the repo. + +However, transcrypt is unable to use a random salt, because it requires +encryption to be a deterministic process. Otherwise, git would always see a +changed file every time the "clean" command was executed. + +Transcrypt therefore defines two strategies to generate a deterministic salt: + +1. The "password" salt method. +2. The "configured" salt method. + +The first method is equivalent to the existing process in transcrypt 2.x. +The second method is a new more secure variant, but will rely on a new +"versioned config" that we will discuss in +:ref:`the configuration storage section `. + +The two salt methods are very similar. In both cases, a unique 32-byte salt is +generated for each file via the following invocation: + +.. code:: bash + + # Used to compute salt for a specific file using "extra-salt" that can be supplied in one of several ways + openssl dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16 + +This salt is based on the name of the file, its sha256 hash, and something called "extra-salt", which is determined by the user's choice of ``transcrypt.salt-method``. + +In the case where ``transcrypt.salt-method=password``, the "extra-salt" is set +to the user's plaintext password. This exactly mimics the behavior of +transcrypt 2.x and is used as the default to provide backwards compatibility. + +However, as discussed in `#55 _`, this introduces a security weakness that weakens the extra security provided the use of ``-pbkdf2``. Thus transcrypt 3.x introduces a new "configured" method. + +In the case where ``transcrypt.salt-method=configured``, the implementation +will check if a special configuration variable ``transcrypt.config-salt`` is +set, and if not, it will set it to a random 32 character hex string, and check +the choice of that value into the repo. Then the value of +``transcrypt.config-salt`` will be used as "extra-salt". The value of +``transcrypt.config-salt`` is randomized every time the user changes their +password. We note that this method this method does provide less entropy than +randomly choosing the salt on each encryption cycle, but we are unaware of +any security concerns that arise from this method. + +Note: this method could be further improved by generated a randomized +config-salt for each file that is modified when the file itself is modified. +Such a scheme should exactly match the entropy of the openssl default +randomized salt method. However, due to the added implementation complexity +and unclear security benefits we defer that to future work. + +See Also: + +PKCS5#4.1 (RFC-2898) https://datatracker.ietf.org/doc/html/rfc2898#section-4.1 + +.. _ConfigStorage: + +Configuration Storage +--------------------- + +In transcrypt 2.x, there are currently two ways to store a configuration +containing credentials and + +1. The unversioned config. +2. The GPG-exported config. + +Method 1 stores the configuration in the ``[transcrypt]`` section of the local +``.git/config`` file. This is the primary location for the configuration and +it is typically populated via specifying all settings either via an interactive +process or through non-interactive command line invocation. Whenever transcrypt +is invoked, any needed configuration variable is read from this plaintext file +using git's versatile configuration tool. + +Method 2 is used exclusively for securely transporting configurations between +machines or authorized users. The ``[transcrypt]`` section of an existing +primary configuration in the ``.git/config`` is exported into a simple new line +separated key/value store format, and then encrypted for a specific GPG user. +This encrypted file can be sent to the target recipient. They can then use +transcrypt to "import" the file, which uses +`GPG _` to decrypt the file and +populate their local unversioned ``.git/config`` file. + +In Transcrypt 3.x we propose a third configuration method: + +3. The versioned config. + +Method 3 will store the non-sensitive subset of configuration settings +(everything but ``transcrypt.password``) in a versioned ``.transcrypt/config`` +file using the same git configuration system as Method 1. + +The motivation for this is twofold. + +First, the new deterministic salt method requires a way of storing randomly +sampled bits for the salt (in the ``transcrypt.config-salt`` variable) that are +decorrelated from sensitive information (i.e. the password and contents of +decrypted files). + +Second, transcrypt 3.x adds 4 new parameters that a user will need to +configure. By storing these parameters in the repo itself it will ease the +burden of decrypting a fresh clone of a repo. + +Using this versioned config for everything but ``transcrypt.config-salt`` is +completely optional (and using ``transcrypt.config-salt`` is not needed if +``transcrypt.salt-method=password``, although that is not recommended). Thus +the user can still choose to keep the chosen cipher, digest, and use of pbkdf2 +a secret if they desire (although we will remind the reader that +`security by obscurity _` +should never be relied on). + +NOTE: Currently, as of 2022-05-09, the current implementation of transcrypt 3.x +does not implement the ability for ``.transcrypt/config`` to store any config +variable other than ``transcrypt.config-salt``. We will wait for this proposal +to be reviewed because the design of the priority in which configuration +variables are stored is is currently an open question in the mind of the +author. However, proposed example *behavior* is as follows: + +Case Study and Open Questions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Given: A fresh clone of a repo without a ``.transcrypt/config`` file. + +The user invokes ``transcrypt`` and is prompted for all 6 configuration variables. + +These are stored to the primary ``.git/config`` file, except for +``transcrypt.config-salt``, which --- if the salt method is "configured" --- is +always stored in ``.git/transcrypt`` and checked into the repo. The user is notified +that transcrypt used ``git add`` to stage this file, and instructs the user to commit +the file (transcrypt never invokes the ``git commit`` command). + +Proposal: The user is additionally prompted if they want to add the +non-sensitive configuration to the versioned config. This prompt can be skipped +by specifying ``--versioned=1`` or ``--versioned=0``. In the unversioned case, +the process proceeded as-is, otherwise the non-sensitive configuration is written +to ``.transcrypt/config`` **instead of** being written to ``.git/config``. + +Open Question: When non-sensitive configuration variables are written, should they be: + +1. Written only to ``.transcrypt/config`` and not ``.git/config``? +2. Written to both ``.transcrypt/config`` and ``.git/config``? +3. Written only to ``.transcrypt/config`` and ensured they are removed from ``.git/config``? + +Because all of these configuration files are plain-text and editable we have to +consider the precedence of config settings when loading. The current proposal +is to always look at ``.git/config`` first and then fallback to +``.transcrypt/config``. + +Open Question: When we read a variable from ``.git/config`` and it disagrees +with ``.transcrypt/config`` do we "fix" ``.transcrypt/config``, warn, or ignore +it. My current proposal is to ignore it and rely on documented precedence +rules. From adb24a486ece78eddb9166a9f71fe8a8d99e08db Mon Sep 17 00:00:00 2001 From: joncrall Date: Thu, 19 May 2022 18:25:27 -0400 Subject: [PATCH 15/15] format tweaks --- docs/algorithm.rst | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/algorithm.rst b/docs/algorithm.rst index 83e0b4b..a56b989 100644 --- a/docs/algorithm.rst +++ b/docs/algorithm.rst @@ -59,7 +59,12 @@ The following is the openssl invocation used in encryption ENC_PASS=$password openssl enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS -e -a -S "$salt" "${pbkdf2_args[@]}" -Note: For OpenSSL V3.x, which does not prepend the salt to the ciphertext, we manually prepend the raw salt bytes to the raw openssl output (without ``-a`` for base64 encoding) and then perform base64 encoding of the concatenated text as a secondary task. This makes the output from version 3.x match outputs from the 1.x openssl releases. +Note: For OpenSSL V3.x, which does not prepend the salt to the ciphertext, we +manually prepend the raw salt bytes to the raw openssl output (without ``-a`` +for base64 encoding) and then perform base64 encoding of the concatenated text +as a secondary task. This makes the output from version 3.x match outputs from +the 1.x openssl releases. (Also note: this is now independently patched in +https://github.com/elasticdog/transcrypt/pull/135) The Decryption Process @@ -83,7 +88,7 @@ The following invocation is used for decryption # used to decrypt a file. the cipher, digest, password, and key derivation # function must be known in advance. the salt is always prepended to the # file ciphertext, and ready by openssl, so it does not need to be supplied here. - ENC_PASS=$password openssl enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a + ENC_PASS=$password openssl enc "-${cipher}" -md "${digest}" -pass env:ENC_PASS "${pbkdf2_args[@]}" -d -a Configuration @@ -181,15 +186,20 @@ generated for each file via the following invocation: .. code:: bash # Used to compute salt for a specific file using "extra-salt" that can be supplied in one of several ways - openssl dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16 + openssl dgst -hmac "${filename}:${extra_salt}" -sha256 "$filename" | tr -d '\r\n' | tail -c 16 -This salt is based on the name of the file, its sha256 hash, and something called "extra-salt", which is determined by the user's choice of ``transcrypt.salt-method``. +This salt is based on the name of the file, its sha256 hash, and something +called "extra-salt", which is determined by the user's choice of +``transcrypt.salt-method``. In the case where ``transcrypt.salt-method=password``, the "extra-salt" is set to the user's plaintext password. This exactly mimics the behavior of transcrypt 2.x and is used as the default to provide backwards compatibility. -However, as discussed in `#55 _`, this introduces a security weakness that weakens the extra security provided the use of ``-pbkdf2``. Thus transcrypt 3.x introduces a new "configured" method. +However, as discussed in +`#55 _`, this introduces a +security weakness that weakens the extra security provided the use of +``-pbkdf2``. Thus transcrypt 3.x introduces a new "configured" method. In the case where ``transcrypt.salt-method=configured``, the implementation will check if a special configuration variable ``transcrypt.config-salt`` is