diff --git a/LICENSE b/LICENSE index 5ed94e0..d925757 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Copyright (c) 2014-2019, Aaron Bull Schaefer <aaron@elasticdog.com> +Copyright (c) 2020-2023, James Murty <james@murty.co> +Copyright (c) 2014-2020, Aaron Bull Schaefer <aaron@elasticdog.com> Copyright (c) 2011, Woody Gilk <woody.gilk@gmail.com> Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index 2fc8fb0..e04530c 100644 --- a/README.md +++ b/README.md @@ -151,16 +151,26 @@ repository. The owner of the origin repository can dump the credentials for you by running the `--display` command line option: $ transcrypt --display - The current repository was configured using transcrypt v0.2.0 + + The current repository was configured using transcrypt version 3.0.0-alpha1 and has the following configuration: - CONTEXT: default - CIPHER: aes-256-cbc - PASSWORD: correct horse battery staple + GIT_WORK_TREE: transcrypt + GIT_DIR: transcrypt/.git + GIT_ATTRIBUTES: transcrypt/.gitattributes + + CONTEXT: default + CIPHER: aes-256-cbc + DIGEST: sha512 + KDF: pbkdf2 + ITERATIONS: 256_000 + PROJECT SALT: 5J0QY8uOTe7/B9eYSJ2kOy91 + PASSWORD: correct horse battery staple Copy and paste the following command to initialize a cloned repository: - transcrypt -c aes-256-cbc -p 'correct horse battery staple' + transcrypt -c aes-256-cbc -md sha512 -k pbkdf2 -n 256_000 \ + -ps 5J0QY8uOTe7/B9eYSJ2kOy91 -p 'correct horse battery staple' Once transcrypt has stored the matching credentials, it will force a checkout of any exising encrypted files in order to decrypt them. @@ -200,6 +210,25 @@ directory. the symmetric cipher to utilize for encryption; defaults to aes-256-cbc + -md, --digest=DIGEST + the message digest used to hash the salted password; + defaults to sha512 + Use md5 for compatibility with transcrypt versions < 3 + + -k, --kdf=KEY_DERIVATION_FUNCTION + a key-derivation function to use for strongest encryption; + defaults to pbkdf2 + If enabled, all users will need Transcrypt 3+ and modern OpenSSL + + -n, --iter=ITERATIONS + when using a key-derivation function, its number of iterations; + defaults to 256_000 + + -ps, --salt=PROJECT_SALT + when using a key-derivation function, an extra value to + strengthen per-file salt values; + defaults to 18 random base64 characters + -p, --password=PASSWORD the password to derive the key from; defaults to 30 random base64 characters @@ -344,6 +373,7 @@ to encrypt a file \_top-secret* in a "super" context: transcrypt is provided under the terms of the [MIT License](https://en.wikipedia.org/wiki/MIT_License). +Copyright © 2020-2023, [James Murty](mailto:james@murty.co). Copyright © 2014-2020, [Aaron Bull Schaefer](mailto:aaron@elasticdog.com). ## Contributing diff --git a/contrib/packaging/pacman/PKGBUILD b/contrib/packaging/pacman/PKGBUILD index 248a93a..8417706 100644 --- a/contrib/packaging/pacman/PKGBUILD +++ b/contrib/packaging/pacman/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Aaron Bull Schaefer <aaron@elasticdog.com> pkgname=transcrypt -pkgver=2.3.0-pre +pkgver=3.0.0-alpha1 pkgrel=1 pkgdesc='A script to configure transparent encryption of files within a Git repository' arch=('any') diff --git a/sensitive_file b/sensitive_file index 547ad71..8ce6c13 100644 --- a/sensitive_file +++ b/sensitive_file @@ -1,40 +1,40 @@ -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 -L/aeTVBw2BnDs2K48meuFw== +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 +GsaN2D2yI3S3YjuOOIpjHA== diff --git a/tests/_test_helper.bash b/tests/_test_helper.bash index ebf2ba3..4aa43ac 100644 --- a/tests/_test_helper.bash +++ b/tests/_test_helper.bash @@ -39,10 +39,6 @@ function cleanup_all { rm -f "$BATS_TEST_DIRNAME"/sensitive_file } -function init_transcrypt { - "$BATS_TEST_DIRNAME"/../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes -} - function encrypt_named_file { filename="$1" content=$2 @@ -59,10 +55,25 @@ function encrypt_named_file { run git commit -m "Encrypt file \"$filename\"" } +function init_transcrypt_no_kdf { + "$BATS_TEST_DIRNAME"/../transcrypt --cipher=aes-256-cbc --password='abc 123' --yes +} + +function init_transcrypt { + "$BATS_TEST_DIRNAME"/../transcrypt --cipher=aes-256-cbc --digest sha512 --kdf pbkdf2 --iter 99 --salt 5J0Q --password='abc 123' --yes +} + function setup { pushd "$BATS_TEST_DIRNAME" || exit 1 init_git_repo - if [[ ! "$SETUP_SKIP_INIT_TRANSCRYPT" ]]; then + + if [[ "$SETUP_SKIP_INIT_TRANSCRYPT" ]]; then + return + fi + + if [[ "$SETUP_INIT_TRANSCRYPT_NO_KDF" ]]; then + init_transcrypt_no_kdf + else init_transcrypt fi } diff --git a/tests/test_cleanup.bats b/tests/test_cleanup.bats index fc00077..7796960 100755 --- a/tests/test_cleanup.bats +++ b/tests/test_cleanup.bats @@ -3,7 +3,16 @@ load "$BATS_TEST_DIRNAME/_test_helper.bash" SECRET_CONTENT="My secret content" -SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" + +# Example generation: +# - Using project salt: 5J0Q +# - Generate file key +# openssl dgst -hmac "sensitive_file:5J0Q" -sha256 sensitive_file | tr -d '\r\n' | tail -c16 +# => ec32c0fbf2261d18 +# - Encrypt file +# cat sensitive_file | ENC_PASS='abc 123' openssl enc -e -a -aes-256-cbc -md sha512 -pass env:ENC_PASS -pbkdf2 -iter 99 -S ec32c0fbf2261d18 +# => U2FsdGVkX1+NiURgsIjgkwyiBw0TSC8WhhDRly2h4x2exuwjay6y/nOahblrBL62 +SECRET_CONTENT_ENC="U2FsdGVkX1+NiURgsIjgkwyiBw0TSC8WhhDRly2h4x2exuwjay6y/nOahblrBL62" @test "cleanup: transcrypt -f flush clears cached plaintext" { encrypt_named_file sensitive_file "$SECRET_CONTENT" diff --git a/tests/test_contexts.bats b/tests/test_contexts_no_kdf.bats similarity index 98% rename from tests/test_contexts.bats rename to tests/test_contexts_no_kdf.bats index f0f9de7..bd4b7be 100755 --- a/tests/test_contexts.bats +++ b/tests/test_contexts_no_kdf.bats @@ -9,7 +9,7 @@ SUPER_SECRET_CONTENT_ENC="U2FsdGVkX1+dAkIV/LAKXMmqjDNOGoOVK8Rmhw9tUnbR4dwBDglpkX function setup { pushd "$BATS_TEST_DIRNAME" || exit 1 init_git_repo - init_transcrypt + init_transcrypt_no_kdf # Init transcrypt with 'super-secret' context "$BATS_TEST_DIRNAME"/../transcrypt --context=super-secret --cipher=aes-256-cbc --password=321cba --yes @@ -86,9 +86,9 @@ function teardown { [ "$status" -eq 0 ] [ "${lines[0]}" = "The current repository was configured using transcrypt version $VERSION" ] [ "${lines[1]}" = "and has the following configuration for context 'super-secret':" ] - [ "${lines[5]}" = " CONTEXT: super-secret" ] - [ "${lines[6]}" = " CIPHER: aes-256-cbc" ] - [ "${lines[7]}" = " PASSWORD: 321cba" ] + [ "${lines[5]}" = " CONTEXT: super-secret" ] + [ "${lines[6]}" = " CIPHER: aes-256-cbc" ] + [ "${lines[7]}" = " PASSWORD: 321cba" ] [ "${lines[8]}" = "The repository has 2 contexts: default super-secret" ] [ "${lines[9]}" = "Copy and paste the following command to initialize a cloned repository for context 'super-secret':" ] [ "${lines[10]}" = " transcrypt -C super-secret -c aes-256-cbc -p '321cba'" ] diff --git a/tests/test_crypt.bats b/tests/test_crypt_no_kdf.bats similarity index 99% rename from tests/test_crypt.bats rename to tests/test_crypt_no_kdf.bats index 1186843..24feac6 100755 --- a/tests/test_crypt.bats +++ b/tests/test_crypt_no_kdf.bats @@ -2,6 +2,9 @@ load "$BATS_TEST_DIRNAME/_test_helper.bash" +# Custom setup: use no-KDF init transcrypt +SETUP_INIT_TRANSCRYPT_NO_KDF=1 + SECRET_CONTENT="My secret content" SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" diff --git a/tests/test_init.bats b/tests/test_init_no_kdf.bats similarity index 90% rename from tests/test_init.bats rename to tests/test_init_no_kdf.bats index cbae5b2..74f039a 100755 --- a/tests/test_init.bats +++ b/tests/test_init_no_kdf.bats @@ -15,20 +15,20 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 } @test "init: creates .gitattributes" { - init_transcrypt + init_transcrypt_no_kdf [ -f .gitattributes ] run cat .gitattributes [ "${lines[0]}" = "#pattern filter=crypt diff=crypt merge=crypt" ] } @test "init: creates scripts in .git/crypt/" { - init_transcrypt + init_transcrypt_no_kdf [ -d .git/crypt ] [ -f .git/crypt/transcrypt ] } @test "init: applies git config" { - init_transcrypt + init_transcrypt_no_kdf VERSION=$(../transcrypt -v | awk '{print $2}') [ "$(git config --get transcrypt.version)" = "$VERSION" ] @@ -53,26 +53,26 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 } @test "init: show details for --display" { - init_transcrypt + init_transcrypt_no_kdf VERSION=$(../transcrypt -v | awk '{print $2}') run ../transcrypt --display [ "$status" -eq 0 ] [[ "${output}" = *"The current repository was configured using transcrypt version $VERSION"* ]] - [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] - [[ "${output}" = *" PASSWORD: abc 123"* ]] + [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] + [[ "${output}" = *" PASSWORD: abc 123"* ]] [[ "${output}" = *" transcrypt -c aes-256-cbc -p 'abc 123'"* ]] } @test "init: show details for -d" { - init_transcrypt + init_transcrypt_no_kdf VERSION=$(../transcrypt -v | awk '{print $2}') run ../transcrypt -d [ "$status" -eq 0 ] [[ "${output}" = *"The current repository was configured using transcrypt version $VERSION"* ]] - [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] - [[ "${output}" = *" PASSWORD: abc 123"* ]] + [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] + [[ "${output}" = *" PASSWORD: abc 123"* ]] [[ "${output}" = *" transcrypt -c aes-256-cbc -p 'abc 123'"* ]] } @@ -80,7 +80,7 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 git config core.hooksPath ".git/myhooks" [ "$(git config --get core.hooksPath)" = '.git/myhooks' ] - init_transcrypt + init_transcrypt_no_kdf [ -d .git/myhooks ] [ -f .git/myhooks/pre-commit ] @@ -88,13 +88,13 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 run ../transcrypt --display [ "$status" -eq 0 ] [[ "${output}" = *"The current repository was configured using transcrypt version $VERSION"* ]] - [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] - [[ "${output}" = *" PASSWORD: abc 123"* ]] + [[ "${output}" = *" CIPHER: aes-256-cbc"* ]] + [[ "${output}" = *" PASSWORD: abc 123"* ]] [[ "${output}" = *" transcrypt -c aes-256-cbc -p 'abc 123'"* ]] } @test "init: transcrypt.openssl-path config setting defaults to 'openssl'" { - init_transcrypt + init_transcrypt_no_kdf [ "$(git config --get transcrypt.openssl-path)" = 'openssl' ] } @@ -104,7 +104,7 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 } @test "init: --set-openssl-path is applied during upgrade" { - init_transcrypt + init_transcrypt_no_kdf [ "$(git config --get transcrypt.openssl-path)" = 'openssl' ] # Set openssl path @@ -116,7 +116,7 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 } @test "init: transcrypt.openssl-path config setting is retained with --upgrade" { - init_transcrypt + init_transcrypt_no_kdf [ "$(git config --get transcrypt.openssl-path)" = 'openssl' ] # Set openssl path @@ -136,7 +136,7 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 # Set a custom location for the crypt/ directory git config transcrypt.crypt-dir /tmp/crypt - init_transcrypt + init_transcrypt_no_kdf # Confirm crypt/ directory is populated in custom location [ ! -d .git/crypt ] @@ -152,7 +152,7 @@ SETUP_SKIP_INIT_TRANSCRYPT=1 # Set a custom location for the crypt/ directory git config transcrypt.crypt-dir /tmp/crypt - init_transcrypt + init_transcrypt_no_kdf SECRET_CONTENT="My secret content" SECRET_CONTENT_ENC="U2FsdGVkX1/6ilR0PmJpAyCF7iG3+k4aBwbgVd48WaQXznsg42nXbQrlWsf/qiCg" diff --git a/transcrypt b/transcrypt index 92aae3f..899f814 100755 --- a/transcrypt +++ b/transcrypt @@ -8,7 +8,9 @@ set -euo pipefail # a Git repository. It utilizes OpenSSL's symmetric cipher routines and follows # the gitattributes(5) man page regarding the use of filters. # -# Copyright (c) 2014-2019 Aaron Bull Schaefer <aaron@elasticdog.com> +# Copyright (c) 2020-2023 James Murty <james@murty.co> +# Copyright (c) 2014-2020 Aaron Bull Schaefer <aaron@elasticdog.com> +# # This source code is provided under the terms of the MIT License # that can be be found in the LICENSE file. # @@ -22,14 +24,26 @@ GREP_OPTIONS="" ##### CONSTANTS # the release version of this script -readonly VERSION='2.3.0-pre' +readonly VERSION='3.0.0-alpha1' -# the default cipher to utilize +# the default encryption settings to recommend readonly DEFAULT_CIPHER='aes-256-cbc' +readonly DEFAULT_DIGEST='sha512' +#readonly DEFAULT_KDF='pbkdf2' # Commented out for now +# See OWASP PBKDF2 iteration count recommedations: +# https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 +readonly DEFAULT_ITERATIONS='256_000' # context name must match this regexp to ensure it is safe for git config and attrs readonly CONTEXT_REGEX='[a-z](-?[a-z0-9])*' +# Encrypted files have the prefix "Salted__" ("U2FsdGVk" in Base64) due to an +# OpenSSL convention. We rely on this marker to check if a file is already +# encrypted. Later versions of OpenSSL have dropped this convention, so we must +# sometimes add the prefix back in (see #133 #147). +readonly OPENSSL_ENCRYPTED_SALTED_PREFIX='Salted__' +readonly OPENSSL_ENCRYPTED_SALTED_PREFIX_B64='U2FsdGVk' + ##### FUNCTIONS # load encryption password @@ -49,6 +63,23 @@ save_password() { git config "transcrypt${context_config_group}.password" "$password" } +# load project salt +# by default is stored in git config, modify this function to move elsewhere +load_project_salt() { + local context_config_group=${1:-} + local project_salt + project_salt=$(git config --get --local "transcrypt${context_config_group}.project-salt") + echo "$project_salt" +} + +# save project salt +# by default is stored in git config, modify this function to move elsewhere +save_project_salt() { + local project_salt=$1 + local context_config_group=${2:-} + git config "transcrypt${context_config_group}.project-salt" "$project_salt" +} + # print a canonicalized absolute pathname realpath() { local path=$1 @@ -162,7 +193,8 @@ derive_context_config_group() { } # Detect OpenSSL major version 3 or later which requires a compatibility -# work-around to include the prefix 'Salted__' and salt value when encrypting. +# work-around to include the prefix 'Salted__' before the salt value when +# encrypting. # # Note that the LibreSSL project's version of the openssl command does NOT # require this work-around for major version 3. @@ -181,79 +213,139 @@ is_salt_prefix_workaround_required() { fi } -# The `decryption -> encryption` process on an unchanged file must be -# deterministic for everything to work transparently. To do that, the same -# salt must be used each time we encrypt the same file. An HMAC has been -# proven to be a PRF, so we generate an HMAC-SHA256 for each decrypted file -# (keyed with a combination of the filename and transcrypt password), and -# then use the last 16 bytes of that HMAC for the file's unique salt. +# Read transcrypt configuration from the Git config and translate the options +# into a "base" OpenSSL command that can then be used to encrypt or decrypt a +# file with minimal extra arguments, as appropriate. +_translate_transcrypt_config_to_openssl_base_arguments() { + context=$(extract_context_name_from_name_value_arg "${1:-}") + context_config_group=$(derive_context_config_group "$context") + + cipher=$(git config --get --local "transcrypt${context_config_group}.cipher" || printf '') + digest=$(git config --get --local "transcrypt${context_config_group}.digest" || printf 'md5') # md5 for legacy compatibility + password=$(load_password "$context_config_group") + + kdf=$(git config --get --local "transcrypt${context_config_group}.kdf" || printf '') + iterations=$(git config --get --local "transcrypt${context_config_group}.iterations" || printf '') + project_salt=$(git config --get --local "transcrypt${context_config_group}.project-salt" || printf '') + + openssl_path=$(git config --get --local transcrypt.openssl-path || printf '') + + # Strip "_" separator characters from iterations value + iterations=${iterations//_/} + # TODO validate $kdf and $digest + # TODO assert $iterations and $project_salt are set when $kdf is applied + + _openssl_base_arguments="ENC_PASS=\"${password}\" ${openssl_path} enc -pass env:ENC_PASS -${cipher} -md ${digest} ${kdf:+-${kdf}} ${iterations:+-iter ${iterations}}" +} + +_encrypt_file() { + local filepath="$1" + local salt="$2" + + if [ "$(is_salt_prefix_workaround_required)" == "true" ]; then + # Encrypt the file to base64, ensuring it includes the prefix 'Salted__' with the salt. #133 + ( + echo -n "$OPENSSL_ENCRYPTED_SALTED_PREFIX" && echo -n "$salt" | xxd -r -p && + # Encrypt file to binary ciphertext + eval "${_openssl_base_arguments} -e -S ${salt} -in ${filepath}" + ) | + openssl base64 + else + # Encrypt file to base64 ciphertext + eval "${_openssl_base_arguments} -e -a -S ${salt} -in ${filepath}" + fi +} + +############################################################################## +# The `decryption -> encryption` process on an unchanged file must be +# deterministic for everything to work transparently. To do that, the same salt +# must be used each time we encrypt the same file. An HMAC has been proven to +# be a PRF, so we generate an HMAC-SHA256 for each decrypted file and then use +# the last 16 bytes of that HMAC for the file's unique salt (the openssl +# standard for salt is 16 hex bytes). +# +# The HMAC-SHA256 is keyed with a combination of the filename and... +# - if a key-derivation function is NOT set, the transcrypt password +# - if a key-derivation function IS set, a "project salt" value that is set +# when the repository is first initialised, from a value provided or by +# generating a random value, and stored in the Git config. This project salt +# must be provided along with the password to decrypt the repo. +############################################################################## + +# The clean script ENCRYPTS files into the Git index, before they are sent to +# the remote. The file contents are passed via stdin and the filename is passed +# as $1 (or $2 if $1 is context name definition). git_clean() { - context=$(extract_context_name_from_name_value_arg "$1") - [[ "$context" ]] && shift + _translate_transcrypt_config_to_openssl_base_arguments "$1" + + # We may or may not get a context argument as $1. Filename is $2 if we do. + if [[ "$context" ]]; then + filename=$2 + else + filename=$1 + fi - 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) firstbytes=$(head -c8 "$tempfile" | LC_ALL=C tr -d '\0') - if [[ $firstbytes == "U2FsdGVk" ]]; then + if [[ $firstbytes == "$OPENSSL_ENCRYPTED_SALTED_PREFIX_B64" ]]; then cat "$tempfile" else - context_config_group=$(derive_context_config_group "$context") - cipher=$(git config --get --local "transcrypt${context_config_group}.cipher") - password=$(load_password "$context_config_group") - openssl_path=$(git config --get --local transcrypt.openssl-path) - salt=$("${openssl_path}" dgst -hmac "${filename}:${password}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16) - - if [ "$(is_salt_prefix_workaround_required)" == "true" ]; then - # Encrypt the file to base64, ensuring it includes the prefix 'Salted__' with the salt. #133 - ( - echo -n "Salted__" && echo -n "$salt" | xxd -r -p && - # Encrypt file to binary ciphertext - ENC_PASS=$password "$openssl_path" enc -e "-${cipher}" -md MD5 -pass env:ENC_PASS -S "$salt" -in "$tempfile" - ) | - openssl base64 - else - # Encrypt file to base64 ciphertext - ENC_PASS=$password "$openssl_path" enc -e -a "-${cipher}" -md MD5 -pass env:ENC_PASS -S "$salt" -in "$tempfile" - fi + # Use "project salt" as extra salt when available (i.e. when a + # key-derivation function is set) otherwise the password. + extra_salt="${project_salt:-${password}}" + + file_salt=$("${openssl_path}" dgst -hmac "${filename}:${extra_salt}" -sha256 "$tempfile" | tr -d '\r\n' | tail -c16) + + _encrypt_file "$tempfile" "$file_salt" fi } +# The smudge script DECRYPTS files when they are checked out to working copy +# files. The file contents are passed via stdin git_smudge() { tempfile=$(mktemp 2>/dev/null || mktemp -t tmp) trap 'rm -f "$tempfile"' EXIT - context=$(extract_context_name_from_name_value_arg "$1") - context_config_group=$(derive_context_config_group "$context") - cipher=$(git config --get --local "transcrypt${context_config_group}.cipher") - password=$(load_password "$context_config_group") - openssl_path=$(git config --get --local transcrypt.openssl-path) - tee "$tempfile" | ENC_PASS=$password "$openssl_path" enc -d "-${cipher}" -md MD5 -pass env:ENC_PASS -a 2>/dev/null || cat "$tempfile" + + _translate_transcrypt_config_to_openssl_base_arguments "${1:-}" + + tee "$tempfile" | eval "${_openssl_base_arguments} -d -a" 2>/dev/null || cat "$tempfile" } +# 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. +# The filename is passed as $1 (or $2 if $1 is context name definition). git_textconv() { - context=$(extract_context_name_from_name_value_arg "$1") - [[ "$context" ]] && shift + _translate_transcrypt_config_to_openssl_base_arguments "$1" + + # We may or may not get a context argument as $1. Filename is $2 if we do. + if [[ "$context" ]]; then + filename=$2 + else + filename=$1 + fi - filename=$1 # ignore empty files if [[ ! -s $filename ]]; then return fi - context_config_group=$(derive_context_config_group "$context") - cipher=$(git config --get --local "transcrypt${context_config_group}.cipher") - password=$(load_password "$context_config_group") - openssl_path=$(git config --get --local transcrypt.openssl-path) - ENC_PASS=$password "$openssl_path" enc -d "-${cipher}" -md MD5 -pass env:ENC_PASS -a -in "$filename" 2>/dev/null || cat "$filename" + eval "${_openssl_base_arguments} -d -a -in \"${filename}\"" 2>/dev/null || cat "$filename" } # shellcheck disable=SC2005,SC2002,SC2181 @@ -317,7 +409,7 @@ git_pre_commit() { if [[ $firstbytes == "" ]]; then : # Do nothing # The first bytes of an encrypted file must be "Salted" in Base64 - elif [[ $firstbytes != "U2FsdGVk" ]]; then + elif [[ $firstbytes != "$OPENSSL_ENCRYPTED_SALTED_PREFIX_B64" ]]; then printf 'Transcrypt managed file is not encrypted in the Git index: %s\n' "$secret_file" >&2 printf '\n' >&2 printf 'You probably staged this file using a tool that does not apply' >&2 @@ -344,7 +436,7 @@ git_pre_commit() { # Get prefix of raw file in Git's index using the :FILENAME revision syntax # The first bytes of an encrypted file are always "Salted" in Base64 local firstbytes=$(git show :"${secret_file}" | head -c8) - if [[ $firstbytes != "U2FsdGVk" ]]; then + if [[ $firstbytes != "$OPENSSL_ENCRYPTED_SALTED_PREFIX_B64" ]]; then echo "true" >>"${tmp}" fi } @@ -420,52 +512,144 @@ run_safety_checks() { fi } +# 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 +} + +# Checks if the target variable is in the set of valid values. If it is not, it +# unsets the global 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:\n%s' "$varname" "$varval" "$valid_values") + if [[ $interactive ]]; then + _set_global "$varname" "" + echo "$message" + return 1 + else + die 1 "$message" + fi + fi +} + # unset the cipher variable if it is not supported by openssl validate_cipher() { - local list_cipher_commands + local valid_ciphers if "${openssl_path}" list-cipher-commands &>/dev/null; then - # OpenSSL < v1.1.0 - list_cipher_commands="${openssl_path} list-cipher-commands" + # OpenSSL < v1.1.0 or LibreSSL + valid_ciphers=$("${openssl_path}" list-cipher-commands) else # OpenSSL >= v1.1.0 - list_cipher_commands="${openssl_path} list -cipher-commands" + valid_ciphers=$("${openssl_path}" list -cipher-commands) fi - local supported - supported=$($list_cipher_commands | tr -s ' ' '\n' | grep -Fx "$cipher") || true - if [[ ! $supported ]]; then + # Force global variable value to lowercase + cipher=$(echo "${cipher}" | tr '[:upper:]' '[:lower:]') + + _validate_variable_str "cipher" "$valid_ciphers" +} + +validate_digest() { + local valid_digests + if "${openssl_path}" list-message-digest-commands &>/dev/null; then + # LibreSSL + # BEWARE: LibreSSL return error code 0 for faulty list commands so we + # must try its command variant first to avoid accepting the error + # output we would get from the other OpenSSL command variants + valid_digests=$("${openssl_path}" list-message-digest-commands) + elif "${openssl_path}" list-digest-commands &>/dev/null; then + # OpenSSL < v1.1.0 + valid_digests=$("${openssl_path}" list-digest-commands) + elif "${openssl_path}" list -digest-commands &>/dev/null; then + # OpenSSL >= v1.1.0 + valid_digests=$("${openssl_path}" list -digest-commands) + fi + + # Force global variable value to lowercase + digest=$(echo "${digest}" | tr '[:upper:]' '[:lower:]') + + _validate_variable_str "digest" "$valid_digests" +} + +# Only the "pbkdf2" key-derivation function is currently supported +validate_kdf() { + local valid_kdfs + valid_kdfs="pbkdf2" + + # Force global variable value to lowercase + kdf=$(echo "${kdf}" | tr '[:upper:]' '[:lower:]') + + _validate_variable_str "kdf" "$valid_kdfs" +} + +# Iteration count must be a positive integer +validate_iterations() { + # Strip "_" separator characters from iterations value + local iterations_value + iterations_value=${iterations//_/} + + if test "$iterations_value" -gt 0 2>/dev/null; then + : # Valid integer > 0 + else + message=$(printf 'iterations is "%s", but must be a positive integer' "$iterations_value") 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='' + _set_global "$varname" "" + echo "$message" + return 1 else - # shellcheck disable=SC2016 - die 1 '"%s" is not a valid cipher; see `%s`' "$cipher" "$list_cipher_commands" + die 1 "$message" fi fi } -# ensure we have a cipher to encrypt with -get_cipher() { - while [[ ! $cipher ]]; do +# 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 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER" + printf '%s' "$prompt" read -r answer fi - - # use the default cipher if the user gave no answer; - # otherwise verify the given cipher is supported by openssl + # 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 - cipher=$DEFAULT_CIPHER + _set_global "$varname" "$default" else - cipher=$answer - validate_cipher + _set_global "$varname" "$answer" + fi + + # Run validate function if provided. + if [[ $validate_fn ]]; then + # In interactive mode, failed validation just clears the variable + # to permit re-entry within the while loop + if [[ $interactive ]]; then + ${validate_fn} || _set_global "$varname" "" + # In non-interactive mode, failed validation causes script to die + else + ${validate_fn} || die "Invalid setting" + fi fi done } +# sets a bash global variable by name +_set_global() { + key=$1 + val=$2 + printf -v "$key" '%s' "$val" +} + # ensure we have a password to encrypt with get_password() { while [[ ! $password ]]; do @@ -491,6 +675,30 @@ get_password() { done } +# ensure we have a project salt to encrypt with when using a key-derivation function +get_project_salt() { + while [[ ! $project_salt ]]; do + printf 'Projects using a key-derivation function require a project salt\n' + local answer= + if [[ $interactive ]]; then + printf 'Generate a random project salt? [Y/n] ' + read -r -n 1 -s answer + printf '\n' + fi + + # generate a random project salt value if the user answered yes; + # otherwise prompt the user for the hex salt value + if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then + local project_salt_length=18 # 18 bytes gives a 25 char B64 string + project_salt=$(${openssl_path} rand -base64 $project_salt_length) + else + printf 'Project salt: ' + read -r project_salt + [[ $project_salt ]] || printf 'no project salt was specified\n' + fi + done +} + # confirm the transcrypt configuration confirm_configuration() { local answer= @@ -500,9 +708,13 @@ confirm_configuration() { 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 ' CONTEXT: %s\n' "$CONTEXT" - printf ' CIPHER: %s\n' "$cipher" - printf ' PASSWORD: %s\n\n' "$password" + printf ' CONTEXT: %s\n' "$CONTEXT" + printf ' CIPHER: %s\n' "$cipher" + [[ "${digest:-}" ]] && printf ' DIGEST: %s\n' "$digest" + [[ "${kdf:-}" ]] && printf ' KDF: %s\n' "$kdf" + [[ "${iterations:-}" ]] && printf ' ITERATIONS: %s\n' "$iterations" + [[ "${project_salt:-}" ]] && printf ' PROJECT SALT: %s\n' "$project_salt" + printf ' PASSWORD: %s\n\n' "$password" printf 'Does this look correct? [Y/n] ' read -r -n 1 -s answer @@ -524,9 +736,13 @@ confirm_rekey() { 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 ' CONTEXT: %s\n' "$CONTEXT" - printf ' CIPHER: %s\n' "$cipher" - printf ' PASSWORD: %s\n\n' "$password" + printf ' CONTEXT: %s\n' "$CONTEXT" + printf ' CIPHER: %s\n' "$cipher" + [[ "${digest:-}" ]] && printf ' DIGEST: %s\n' "$digest" + [[ "${kdf:-}" ]] && printf ' KDF: %s\n' "$kdf" + [[ "${iterations:-}" ]] && printf ' ITERATIONS: %s\n' "$iterations" + [[ "${project_salt:-}" ]] && printf ' PROJECT SALT: %s\n' "$project_salt" + printf ' PASSWORD: %s\n\n' "$password" printf 'You are about to re-encrypt all encrypted files using new credentials.\n' printf 'Once you do this, their historical diffs will no longer display in plain text.\n\n' printf 'Proceed with rekey? [y/N] ' @@ -631,7 +847,16 @@ save_configuration() { # write the encryption info git config transcrypt.version "$VERSION" git config "transcrypt${CONTEXT_CONFIG_GROUP}.cipher" "$cipher" + [[ ${digest:-} ]] && git config "transcrypt${CONTEXT_CONFIG_GROUP}.digest" "$digest" + [[ ${kdf:-} ]] && git config "transcrypt${CONTEXT_CONFIG_GROUP}.kdf" "$kdf" + [[ ${iterations:-} ]] && git config "transcrypt${CONTEXT_CONFIG_GROUP}.iterations" "$iterations" save_password "$password" "$CONTEXT_CONFIG_GROUP" + + # if a key derivation function is specified, generate a project salt to use when encrypting files + if [[ ${kdf:-} ]]; then + save_project_salt "$project_salt" "$CONTEXT_CONFIG_GROUP" + fi + git config transcrypt.openssl-path "$openssl_path" # write the filter settings. Sorry for the horrific quote escaping below... @@ -673,8 +898,17 @@ save_configuration() { # display the current configuration settings display_configuration() { - local current_cipher - current_cipher=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.cipher") + local cipher + cipher=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.cipher") + local digest + digest=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.digest" || printf '') + local kdf + kdf=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.kdf" || printf '') + local iterations + iterations=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.iterations" || printf '') + + local project_salt + project_salt=$(load_project_salt "$CONTEXT_CONFIG_GROUP") local current_password current_password=$(load_password "$CONTEXT_CONFIG_GROUP") local escaped_password=${current_password//\'/\'\\\'\'} @@ -686,18 +920,22 @@ display_configuration() { [[ ! $REPO ]] || printf ' GIT_WORK_TREE: %s\n' "$REPO" printf ' GIT_DIR: %s\n' "$GIT_DIR" printf ' GIT_ATTRIBUTES: %s\n\n' "$GIT_ATTRIBUTES" - printf ' CONTEXT: %s\n' "$CONTEXT" - printf ' CIPHER: %s\n' "$current_cipher" - printf ' PASSWORD: %s\n\n' "$current_password" + printf ' CONTEXT: %s\n' "$CONTEXT" + printf ' CIPHER: %s\n' "$cipher" + [[ "${digest:-}" ]] && printf ' DIGEST: %s\n' "$digest" + [[ "${kdf:-}" ]] && printf ' KDF: %s\n' "$kdf" + [[ "${iterations:-}" ]] && printf ' ITERATIONS: %s\n' "$iterations" + [[ "${project_salt:-}" ]] && printf ' PROJECT SALT: %s\n' "$project_salt" + printf ' PASSWORD: %s\n\n' "$current_password" if [[ "$contexts_count" -gt "1" ]]; then printf 'The repository has %s contexts: %s\n\n' "$contexts_count" "$CONFIGURED_CONTEXTS" fi printf "Copy and paste the following command to initialize a cloned repository%s:\n\n" "$CONTEXT_DESCRIPTION" if [[ $CONTEXT != 'default' ]]; then - printf " transcrypt -C $CONTEXT -c %s -p '%s'\n" "$current_cipher" "$escaped_password" - else - printf " transcrypt -c %s -p '%s'\n" "$current_cipher" "$escaped_password" + context_extra=" -C $CONTEXT" fi + + printf " transcrypt${context_extra:-} -c ${cipher}${digest:+ -md ${digest}}${kdf:+ -k ${kdf}}${iterations:+ -n ${iterations}}${project_salt:+ -ps ${project_salt}} -p '%s'\n" "$escaped_password" } # remove transcrypt-related settings from the repository's git config @@ -821,9 +1059,9 @@ uninstall_transcrypt() { pre_commit_hook="${GIT_HOOKS}/pre-commit" pre_commit_hook_installed="${GIT_HOOKS}/pre-commit-crypt" if [[ -f "$pre_commit_hook" ]]; then - hook_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook") - installed_md5=$("${openssl_path}" md5 -hex <"$pre_commit_hook_installed") - if [[ "$hook_md5" = "$installed_md5" ]]; then + hook_sha1=$("${openssl_path}" sha1 -hex <"$pre_commit_hook") + installed_sha1=$("${openssl_path}" sha1 -hex <"$pre_commit_hook_installed") + if [[ "$hook_sha1" = "$installed_sha1" ]]; then rm "$pre_commit_hook" else printf 'WARNING: Cannot safely disable Git pre-commit hook %s please check it yourself\n' "$pre_commit_hook" @@ -903,6 +1141,8 @@ upgrade_transcrypt() { fi fi + # TODO Retain new PBKDF2 settings across upgrade + # Keep current cipher and password cipher=$(git config --get --local "transcrypt${CONTEXT_CONFIG_GROUP}.cipher") password=$(load_password "$CONTEXT_CONFIG_GROUP") @@ -1170,6 +1410,25 @@ help() { the symmetric cipher to utilize for encryption; defaults to aes-256-cbc + -md, --digest=DIGEST + the message digest used to hash the salted password; + defaults to sha512 + Use md5 for compatibility with transcrypt versions < 3 + + -k, --kdf=KEY_DERIVATION_FUNCTION + a key-derivation function to use for strongest encryption; + defaults to pbkdf2 + If enabled, all users will need Transcrypt 3+ and modern OpenSSL + + -n, --iter=ITERATIONS + when using a key-derivation function, its number of iterations; + defaults to 256_000 + + -ps, --salt=PROJECT_SALT + when using a key-derivation function, an extra value to + strengthen per-file salt values; + defaults to 18 random base64 characters + -p, --password=PASSWORD the password to derive the key from; defaults to 30 random base64 characters @@ -1313,11 +1572,13 @@ gpg_import_file='' gpg_recipient='' interactive='true' list='' +project_salt='' password='' rekey='' show_file='' uninstall='' upgrade='' +set_openssl_path='' openssl_path='openssl' # used to bypass certain safety checks @@ -1359,6 +1620,34 @@ while [[ "${1:-}" != '' ]]; do --cipher=*) cipher=${1#*=} ;; + -md | --digest) + digest=$2 + shift + ;; + --digest=*) + digest=${1#*=} + ;; + -k | --kdf) + kdf=${2} + shift + ;; + --kdf=*) + kdf=${1#*=} + ;; + -n | --iter) + iterations=${2} + shift + ;; + --iter=*) + iterations=${1#*=} + ;; + -ps | --salt) + project_salt=${2} + shift + ;; + --salt=*) + project_salt=${1#*=} + ;; -p | --password) password=$2 shift @@ -1374,6 +1663,7 @@ while [[ "${1:-}" != '' ]]; do context=${1#*=} ;; --set-openssl-path=*) + set_openssl_path='true' openssl_path=${1#*=} # Immediately apply config setting git config transcrypt.openssl-path "$openssl_path" @@ -1522,10 +1812,58 @@ elif [[ $gpg_import_file ]]; then import_gpg elif [[ $cipher ]]; then validate_cipher +elif [[ ${digest:-} ]]; then + validate_digest +elif [[ ${kdf:-} ]]; then + validate_kdf +elif [[ ${iterations:-} ]]; then + validate_iterations +fi + +# Try to detect when user is initialising transcrypt using a command format +# that does not specify a KDF or related settings, e.g. from before version 3. +if [[ ${cipher:-} ]] && [[ ${password:-} ]] && [[ -z ${digest:-} ]] && [[ -z ${kdf:-} ]] && [[ -z ${iterations:-} ]]; then + is_no_kdf_init_command='true' +else + is_no_kdf_init_command='' fi # perform function calls to configure transcrypt -get_cipher +_get_user_input cipher "$DEFAULT_CIPHER" "validate_cipher" \ + "$(printf 'Encrypt using which cipher? [%s] ' "$DEFAULT_CIPHER")" + +# Prompt user for encryption settings available since version 3, but only if: +# - it doesn't seem like they are using a legacy init command format, or just +# one that doesn't specify a KDF +# - the user isn't just setting the openssl-path +if [[ "$is_no_kdf_init_command" == "" ]] && [[ -z ${set_openssl_path:-} ]]; then + _get_user_input digest "$DEFAULT_DIGEST" "validate_digest" \ + "$(printf 'Encrypt using which digest? [%s] ' "$DEFAULT_DIGEST")" + + if [[ -z ${kdf:-} ]]; then + printf 'Use the PBKDF2 key derivation function for best security?\n' + printf ' * Requires Transcrypt 3+ and modern OpenSSL *\n' + printf '[Y/n] ' + read -r -n 1 -s answer + printf '\n' + else + answer='y' + fi + + if [[ $answer =~ $YES_REGEX ]] || [[ ! $answer ]]; then + # Only KDF currently supported is PBKDF2 so no need to prompt for it + kdf='pbkdf2' + + # _get_user_input kdf "$DEFAULT_KDF" "validate_kdf" \ + # "$(printf 'Which key derivation function? [%s] ' "$DEFAULT_KDF")" + + _get_user_input iterations "$DEFAULT_ITERATIONS" "validate_iterations" \ + "$(printf 'How many iterations? Use "_" separators for clarity [%s] ' "$DEFAULT_ITERATIONS")" + + get_project_salt + fi +fi + get_password if [[ $rekey ]] && [[ $interactive ]]; then