#!/bin/bash # # gitprotect.sh # # Copyright (C) 2013, Branko Majic <branko@majic.rs> # # 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 <http://www.gnu.org/licenses/>. # program="gitprotect.sh" version="0.1" function usage() { cat <<EOF $program $version, a utility for managing GPG-protected directories in a git repository. Usage: $program [OPTIONS] command $program is a utility for managing GPG-protected directories in a git repository. The main intention of the utility is to provide a viable solution for storing the passwords centrally, allowing small geographically disperse teams to exchange them in a secure manner. The utility relies on using GnuPG utility for performing all tasks related to encryption and decryption. The script works on induvidual directories by keeping the GnuPG keyring used for encryption in the subdirectory .gnupg, and using a sub-directory 'decrypted' for storing unencrypted content. The following commands are provided for managing the repository/directories: init Initialises the current directory of a git repository, setting it up for use with gitencrypt. addkey Adds keys to directory's GnuPG public keyring, and marks them as trusted. Command expects one or more positional arguments which should be either files containing an OpenPGP public key, or key identifiers from user's own keyring. rmkey Removes one or more keys from the git repository GnuPG keyring. Command expects one or more positional arguments which should be key identifiers from the git repository directory's GnuPG keyring. listkeys Lists the keys from the git repository GnuPG keyring. encrypt Encrypts all the files from the 'decrypted' sub-directory, and stores them in the initialised directory. The encrypted files will have the .gpg extension. decrypt Decrypts all the files from the current directory, storing them in the 'decrypted' sub-directory. Only the files with extension .gpg are decrypted. $program accepts the following options: -v show script version and licensing information -h show usage help Please report bugs and send feature requests to <branko@majic.rs>. EOF } function version() { cat <<EOF $program, version $version +-----------------------------------------------------------------------+ | Copyright (C) 2013, Branko Majic <branko@majic.rs> | | | | 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 <http://www.gnu.org/licenses/>. | +-----------------------------------------------------------------------+ 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 <<EOF >> .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 <<EOF $program has set-up the repository directory for encryption. Before proceeding, please commit the changes. The commit includes empty public and trust keryings for GnuPG, and gitignore file that prevents inclusion of decrypted files and temporary GnuPG files. Before proceeding with the commit, verify the changes with: git status . git diff --staged . After you have verfied the changes, commit the changes with (you may specify alternative message): git commit .gnupg .gitignore -m "Configured directory for use with gitprotect.sh" EOF elif [[ $command == "addkey" ]]; 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 # 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