Files
@ 42155469350e
Branch filter:
Location: majic-scripts/openpgp/gitprotect.sh
42155469350e
13.5 KiB
text/x-sh
Noticket: Added a small script for creating quick backups of files before making changes to them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 | #!/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
|