Last active
October 6, 2016 14:32
-
-
Save gcv/b7d72023a851f2edf951 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Currently only tested on zsh. | |
# To install, just copy this function to your .zshrc file. | |
# On Mac OS, make sure you have greadlink available (install coreutils | |
# through Homebrew). | |
# Make sure you have the GnuPG binary in your path. | |
function gpgd() { | |
# Enables mounting GPG-encrypted tar files as directories. Upon | |
# dismount, updates the encrypted tar file. | |
# | |
# Usage: | |
# gpgd init my-new-archive.gpg recipient1 recipient2 recipient3 | |
# gpgd mount my-new-archive.gpg "~/Secure Files" | |
# ... edit files in "~/Secure Files/my-new-archive" | |
# gpgd umount "~/Secure Files/my-new-archive" | |
# ... after the unmount, my-new-archive.gpg will be updated | |
# and "~/Secure Files/my-new-archive" securely deleted. | |
# | |
# Features: | |
# - securely removes traces of unencrypted files after unmounting | |
# | |
# Limitations: | |
# - currently requires the basename of the archive file to match | |
# the name of the mount point (e.g., my-archive.gpg needs to be | |
# mounted to a directory called my-archive); renaming the archive | |
# will break it; moving the archive when it's mounted will also | |
# break it (not irrevocably, it'll just require editing the | |
# internal .gpgd-archive-file while mounted) | |
# - public-key encryption only; mainly to avoid the hassle of | |
# typing a passphrase on unmount when using GPG in symmetric mode | |
# - written in shell script for convenience of installation, but | |
# the code does suffer for it | |
# - zsh only | |
# - greadlink dependency on Mac OS | |
# - not ideal for larger encrypted directories: sync of encrypted | |
# archives will not allow any block optimizations, secure rm | |
# is fairly slow | |
# environment check | |
if [[ -z $(command -v gpg) ]]; then | |
echo "no gpg found" 1>&2 | |
return 1 | |
fi | |
if [[ -z $(command -v tar) ]]; then | |
echo "no tar found" 1>&2 | |
return 1 | |
fi | |
if [[ -z $(command -v mktemp) ]]; then | |
echo "no mktemp found" 1>&2 | |
return 1 | |
fi | |
local readlink | |
case $(uname) in | |
"Darwin") | |
if [[ -z $(command -v greadlink) ]]; then | |
echo "GNU readlink required; install coreutils from Homebrew" 1>&2 | |
return 1 | |
else | |
readlink="greadlink" | |
fi | |
;; | |
"Linux") | |
readlink="readlink" | |
;; | |
esac | |
function gpgd_help() { | |
echo "usage:" | |
echo " gpgd init <archive-file> recipient+" | |
echo " gpgd mount <archive-file> <mount-point-parent-directory>" | |
echo " gpgd umount <mount-point>" | |
return 0 | |
} | |
function gpgd_init() { | |
local archive_file=$2 | |
local -a recipients | |
local recipient | |
for recipient in ${argv[3,-1]}; do | |
recipients+=${recipient} | |
done | |
if [[ -z "${archive_file}" ]]; then | |
echo "archive required" 1>&2 | |
return 1 | |
fi | |
if [[ -z ${recipients[1]} ]]; then | |
echo "recipients required" 1>&2 | |
return 1 | |
fi | |
local recipients_gpg_arg="" | |
for recipient in ${recipients}; do | |
if gpg --list-key ${recipient} &> /dev/null; then | |
if [[ -z "${recipients_gpg_arg}" ]]; then | |
recipients_gpg_arg="-r ${recipient}" | |
else | |
recipients_gpg_arg="${recipients_gpg_arg} -r ${recipient}" | |
fi | |
else | |
echo "no public key found for ${recipient}, aborting" 1>&2 | |
return 1 | |
fi | |
done | |
local archive_file_dir="$(dirname ${archive_file})" | |
local archive_filename="$(basename ${archive_file})" | |
local archive_filename_noext="${archive_filename%.*}" | |
if [[ "${archive_filename_noext}" == "${archive_filename}" ]]; then | |
echo "an extension to ${archive_filename} is required, consider using .gpg" | |
return 1 | |
fi | |
if [[ -e "${archive_filename_noext}" ]]; then | |
echo "current directory already has ${archive_filename_noext} directory, aborting" 1>&2 | |
return 1 | |
fi | |
if [[ -e "${archive_file}" ]]; then | |
echo "current directory already has ${archive_file}, aborting" 1>&2 | |
return 1 | |
fi | |
mkdir "${archive_filename_noext}" | |
echo ${recipients} > "${archive_filename_noext}/.gpgd-recipients" | |
# XXX: Write to a temporary archive_file in current directory first. | |
# --output does not support file names with spaces. | |
local tmpfile=$(mktemp XXXXX) | |
rm -f "${tmpfile}" | |
tar cz "./${archive_filename_noext}" | gpg --encrypt "${=recipients_gpg_arg}" --output "${tmpfile}" | |
mv "${tmpfile}" "${archive_file}" | |
rm -rfP "${archive_filename_noext}" | |
} | |
function gpgd_check_shared_mount_points() { | |
local check=$1 | |
while : ; do | |
if [[ -e "${check}/.dropbox.cache" ]]; then | |
echo "${check} seems to be managed by Dropbox" 1>&2 | |
exit 1 | |
fi | |
if [[ -e "${check}/.SyncID" ]]; then | |
echo "${check} seems to be managed by BitTorrent Sync" 1>&2 | |
exit 1 | |
fi | |
[[ "/" == "${check}" ]] && break | |
check=$(dirname "${check}") | |
done | |
} | |
function gpgd_mount() { | |
local archive_file="$(${readlink} -f "${2}")" | |
if [[ ! -f "${archive_file}" ]]; then | |
echo "archive required" 1>&2 | |
return 1 | |
fi | |
local mount_point_parent_directory="$(${readlink} -f "${3}")" | |
if [[ ! -w "${mount_point_parent_directory}" ]]; then | |
echo "writeable mount point parent directory required" 1>&2 | |
return 1 | |
fi | |
# NB: This will not help detect the case of a symlink from | |
# a shared directory into a directory into the secure path. | |
gpgd_check_shared_mount_points "${mount_point_parent_directory}" | |
gpgd_check_shared_mount_points "$3" | |
local archive_file_dir="$(dirname ${archive_file})" | |
local archive_filename="$(basename ${archive_file})" | |
local archive_filename_noext="${archive_filename%.*}" | |
if [[ -e "${mount_point_parent_directory}/${archive_filename_noext}" ]]; then | |
echo "${archive_filename_noext} already exists under ${mount_point_parent_directory}; aborting" | |
return 1 | |
fi | |
pushd "${mount_point_parent_directory}" | |
# XXX: Symlink the archive_file into a temporary file in the current directory first: | |
# --decrypt does not support file names with spaces. | |
local tmpfile=$(mktemp XXXXX) | |
rm -f "${tmpfile}" | |
ln -s "${archive_file}" "${tmpfile}" | |
gpg -q --decrypt "${tmpfile}" | tar xk - | |
rm -f "${tmpfile}" | |
if [[ ! -d "${archive_filename_noext}" ]]; then | |
# TODO: Deal with the problem of archive filenames differing from the root of the archive. | |
# This can occur if someone renames either the mount point or the archive filename. | |
echo "SERIOUS ERROR: archive name ${archive_filename_noext} differs from mount point directory name" 1>&2 | |
return 1 | |
fi | |
pushd "${archive_filename_noext}" | |
echo "${archive_file}" > .gpgd-archive-file | |
popd | |
popd | |
} | |
function gpgd_check_a_not_inside_b() { | |
local a="$1" | |
local b="$2" | |
# reduce a until it's equal to b | |
local check=$a | |
while : ; do | |
if [[ "$a" == "$b" ]]; then | |
echo "you seem to be inside the mount point area; aborting" 1>&2 | |
exit 1 | |
fi | |
[[ "/" == "${check}" ]] && break | |
check=$(dirname "${check}") | |
done | |
} | |
function gpgd_umount() { | |
local mount_point="$(${readlink} -f "$2")" | |
if [[ ! -d "${mount_point}" ]]; then | |
echo "${mount_point} does not exist" 1>&2 | |
return 1 | |
fi | |
if [[ ! -e "${mount_point}/.gpgd-archive-file" ]]; then | |
echo "no .gpgd-archive-file found in ${mount_point}" 1>&2 | |
return 1 | |
fi | |
if [[ ! -e "${mount_point}/.gpgd-recipients" ]]; then | |
echo "no .gpgd-recipients found in ${mount_point}" 1>&2 | |
return 1 | |
fi | |
local mount_point_parent_directory="$(dirname "${mount_point}")" | |
local mount_point_basename="$(basename "${mount_point}")" | |
local current_dir_raw="$(pwd)" | |
local current_dir="$(${readlink} -f "${current_dir_raw}")" | |
local archive_path_raw="$(cat "${mount_point}/.gpgd-archive-file")" | |
local archive_path="$(${readlink} -f "${archive_path_raw}")" | |
local archive_filename="$(basename "${archive_path}")" | |
local archive_filename_noext="${archive_filename%.*}" | |
if [[ ! -e "${archive_path}" ]]; then | |
echo "no encrypted archive found at ${archive_path}" 1>&2 | |
return 1 | |
fi | |
if [[ "${mount_point_basename}" != "${archive_filename_noext}" ]]; then | |
echo "mount point ${mount_point_basename} does not match archive filename ${archive_filename_noext}" | |
return 1 | |
fi | |
gpgd_check_a_not_inside_b "${current_dir}" "${mount_point}" | |
local recipients_raw="$(cat "${mount_point}/.gpgd-recipients")" | |
local -a recipients | |
recipients=("${(s/ /)recipients_raw}") | |
# verify recipients, identical to gpgd_init (shell limitations) | |
local recipients_gpg_arg="" | |
for recipient in ${recipients}; do | |
if gpg --list-key ${recipient} &> /dev/null; then | |
if [[ -z "${recipients_gpg_arg}" ]]; then | |
recipients_gpg_arg="-r ${recipient}" | |
else | |
recipients_gpg_arg="${recipients_gpg_arg} -r ${recipient}" | |
fi | |
else | |
echo "no public key found for ${recipient}, aborting" 1>&2 | |
return 1 | |
fi | |
done | |
pushd "${mount_point_parent_directory}" | |
local tmpfile=$(mktemp XXXXX) | |
rm -f "${tmpfile}" | |
tar cz --exclude .gpgd-archive-file "./${mount_point_basename}" | gpg --encrypt "${=recipients_gpg_arg}" --output "${tmpfile}" | |
mv "${tmpfile}" "${archive_path}" | |
rm -rfP "${mount_point}" | |
popd | |
} | |
# command dispatch | |
local cmd=$1 | |
if [[ -z ${cmd} ]]; then | |
cmd="help" | |
fi | |
gpgd_${cmd} $* || return 1 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment