#!/bin/bash # # gitprotect.sh # # Copyright (C) 2013, Branko Majic # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # program="gitprotect.sh" version="0.1" function usage() { cat <. EOF } function version() { cat < | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU General Public License as published by | | the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU General Public License for more details. | | | | You should have received a copy of the GNU General Public License | | along with this program. If not, see . | +-----------------------------------------------------------------------+ EOF } # # Checks if the current directory is a sub-directory of a git repository or not. # # Returns: # 0 if the current directory is a sub-directory of a git repository, 1 # otherwise. # # Outputs: # If the current directory is not a sub-directory of a git repository, it will # output an informative message # function inGit() { # Check if we are inside of a git repository. if ! git rev-parse --git-dir >/dev/null 2>&1; then echo "ERROR: $program must be executed from within a git repository." >&2 echo "Working directory is '$(pwd)'" >&2 return 1 fi return 0 } # # Checks if the current directory has been configured for gitprotect or not. # # Returns: # 0 if the current directory has been configured for use with gitprotect, 1 # otherwise. # # Outputs: # If the current directory has not been configured, it will output an # informative error message. function gitprotectConfigured() { if ! [[ -d "$gnupgHome" ]]; then echo "ERROR: Current directory has not been configured for use with $program." >&2 echo "You can initialise the current directory with command '$program init'." >&2 echo "Working directory is '$(pwd)'." >&2 return 1 fi return 0 } # # Error codes. # ERR_NOTINGIT=10 ERR_NOCONFIG=11 ERR_NOKEYARG=12 ERR_NODECRYPTDIR=13 ERR_NORECIPIENTS=14 # If no arguments were given, just show usage help. if [[ -z $1 ]]; then usage exit 0 fi # Parse the arguments while getopts "vh" opt; do case "$opt" in v) version exit 0;; h) usage exit 0;; *) usage exit 1;; esac done i=$OPTIND shift $(($i-1)) # Read the command parameter. command="$1" shift # Make sure the command is run from within a git repository. inGit || exit "$ERR_NOTINGIT" # Set-up some default values. gnupgHome="$(pwd)/.gnupg" gnupgArgs=("--homedir" "$gnupgHome" "--batch") if [[ $command == "init" ]]; then if [[ -d $gnupgHome ]]; then echo "Directory already set-up." >&2 exit 0 fi # Create the local .gnupg directory. mkdir "$gnupgHome" chmod 700 "$gnupgHome" # Initialise the GnuPG files in local directory. gpg2 "${gnupgArgs[@]}" --list-keys 2>/dev/null # Set-up a .gitignore file that will exclude some temporary files from being # tracked, as well as decrypted files. cat <> .gitignore # BEGIN gitprotect.sh .gnupg/pubring.gpg~ .gnupg/random_seed .gnupg/secring.gpg decrypted/ # END gitprotect.sh EOF # Add the empty keyring and gitignore file to the index so they can be # committed by the user. git add .gnupg/ git add .gitignore cat <&2 exit "$ERR_NOKEYARG" fi # Process all the keys specified. for key in "$@"; do # First try accessing a file by the given key name. Otherwise treat it # as key identifier. if [[ -f $key ]]; then if ! gpg2 "${gnupgArgs[@]}" --import "$key"; then echo "ERROR: Failed to add key from file '$key'." >&2 fi else if ! gpg2 --batch --list-keys "$key" >/dev/null 2>&1; then echo "WARN: Key with identifier '$key' not found in user's GnuPG keyring. Skipping." >&2 else ! gpg2 --batch --armor --export "${key}!" | gpg2 "${gnupgArgs[@]}" --import if [[ ${PIPESTATUS[0]} != 0 ]]; then echo "ERROR: Failed to add key with identifier '$key')." >&2 fi fi fi done elif [[ $command = "rmkey" ]]; then gitprotectConfigured || exit "$ERR_NOCONFIG" # At least one key has to be provided. if [[ "${#@}" == 0 ]]; then echo "ERROR: At least one key file or identifier must be specified" >&2 exit "$ERR_NOKEYARG" fi # Process all the keys specified. for key in "$@"; do if ! gpg2 "${gnupgArgs[@]}" --list-key "$key" 2>/dev/null; then echo "WARN: Key with identifier '$key' not found in git repository directory's GnuPG keyring. Skipping" >&2 elif ! gpg2 "${gnupgArgs[@]}" --yes --delete-key "$key"; then echo "ERROR: Failed to remove the key with identifier '$key'." >&2 fi done elif [[ $command = "listkeys" ]]; then gitprotectConfigured || exit "$ERR_NOCONFIG" gpg2 "${gnupgArgs[@]}" --list-public-keys --keyid-format long elif [[ $command = "encrypt" ]]; then gitprotectConfigured || exit "$ERR_NOCONFIG" # Verify that the directory with unencrypted files exists. if [[ ! -d "decrypted/" ]]; then echo "ERROR: Nothing to encrypt. sub-directory 'decrypted' does not exist." exit "$ERR_NODECRYPTDIR" fi # Set-up the list of recipients. Read the information about each public # sub-key from the local keyring. while read key_validity key_id key_capabilities; do # Only use non-expired sub-keys that have encryption capability. if [[ $key_validity != e && $key_capabilities =~ .*e.* ]]; then recipients+=("$key_id") recipientArgs+=("-r" "$key_id!") fi done < <(gpg2 "${gnupgArgs[@]}" --list-public-keys --with-colons | grep '^sub' | awk 'BEGIN { FS = ":" } ; { print $2, $5, $12 }') # Make sure that we have at least a single recipient. if [[ "${#recipients[@]}" == 0 ]]; then echo "ERROR: No suitable recipients were found in the keyring. Did you forget to add keys?" >&2 exit "$ERR_NORECIPIENTS" fi # Encrypt every file from the decrypted sub-directory. while read decryptedFile; do filename=$(basename "$decryptedFile") encryptedFile="./${filename}.gpg" checksumFile="decrypted/.${filename}.sha256" # If the encrypted file is present, fetch list of keys that were used to # encrypt it, and list of keys in the current keyring so we can compare # them later on. if [[ -f $encryptedFile ]]; then currentFileRecipients=$(gpg2 --status-fd 1 --homedir "$gnupgHome" --quiet --batch --list-only --decrypt "$encryptedFile" | grep ENC_TO | sed -e 's/.*ENC_TO //;s/ .*//' | sort -u) newFileRecipients=$(echo "${recipients[@]}" | tr ' ' '\n' | sort -u) fi # If an encrypted file exists, and its recipient list is outdated, # re-encrypt the decrypted file to get the recipients into sync. if [[ -f $encryptedFile && $currentFileRecipients != $newFileRecipients ]]; then echo "INFO: Encrypting file '$decryptedFile' due to differing recipients in keyring and current encrypted file." cat "$decryptedFile" | gpg2 --trust-model always "${gnupgArgs[@]}" \ --armor "${recipientArgs[@]}" --encrypt > "$encryptedFile" sha256sum "$decryptedFile" > "$checksumFile" # If the checksum file exists, then verify it. This way we detect if # decrypted file has been changed in any way since it has been # decrypted. We should skip unchanged files. elif [[ -f $checksumFile ]] && sha256sum --quiet -c "$checksumFile" > /dev/null 2>&1; then echo "INFO: File $decryptedFile doesn't seem to have been changed. Skipping." # The file was changed, so we need to encrypt new version of it. else echo "INFO: Encrypting new version of file '$decryptedFile'." cat "$decryptedFile" | gpg2 --trust-model always "${gnupgArgs[@]}" \ --armor "${recipientArgs[@]}" --encrypt > "$encryptedFile" sha256sum "$decryptedFile" > "$checksumFile" fi done < <(find "decrypted/" -maxdepth 1 -type f ! -name '.*.sha256') elif [[ $command = "decrypt" ]]; then gitprotectConfigured || exit "$ERR_NOCONFIG" # Create the sub-directory that will contain the decrypted data. mkdir -p "decrypted/" # Process each GnuPG-encrypted file. while read filePath; do filename=$(basename "$filePath" ".gpg") checksumFile="decrypted/.${filename}.sha256" # If the checksum file exists, and it does not match, the destination # file has been modified, so refuse to overwrite it. if [[ -f $checksumFile ]] && ! sha256sum --quiet -c "$checksumFile" > /dev/null 2>&1; then echo "ERROR: Current decrypted file 'decrypted/$filename' seems to have been modified since it was last decrypted." # Decrypt the file. If decryption has failed, we've ended-up with empty # file we need to remove. elif ! gpg2 --quiet --decrypt "$filePath" > "decrypted/$filename"; then echo "ERROR: Failed to decrypt file '$filePath'. No private key available." >&2 rm "decrypted/$filename" # Create a new checksum file for checking if file had been changed by # user or not. else sha256sum "decrypted/$filename" > "decrypted/.${filename}.sha256" fi done < <(find . -maxdepth 1 -name '*.gpg') else echo "ERROR: Unsupported command '$command'" >&2 fi