Skip to content

Instantly share code, notes, and snippets.

@gcv
Last active October 6, 2016 14:32
Show Gist options
  • Save gcv/b7d72023a851f2edf951 to your computer and use it in GitHub Desktop.
Save gcv/b7d72023a851f2edf951 to your computer and use it in GitHub Desktop.
# 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