403 lines
8.3 KiB
Bash
Executable file
403 lines
8.3 KiB
Bash
Executable file
#!/bin/sh
|
|
|
|
# git-remote-gcrypt
|
|
# Copyright 2013 by Ulrik
|
|
# License: GPLv2 or any later version, see http://www.gnu.org/licenses/
|
|
#
|
|
# See README
|
|
|
|
#set -x
|
|
set -e
|
|
LANG=C
|
|
|
|
genkey()
|
|
{
|
|
gpg --armor --gen-rand 1 128 | tr -d \\n
|
|
}
|
|
|
|
pack_hash()
|
|
{
|
|
gpg --print-md SHA224 | tr -d ' ' | tr A-F a-f
|
|
}
|
|
|
|
LOCALDIR="${GIT_DIR:-.git}/remote-gcrypt"
|
|
DID_FIND_REPO= # yes for connected, no for no repo
|
|
PACKPFX="pack :SHA224:"
|
|
|
|
isurl() { test -z "${2%%$1://*}" ; }
|
|
|
|
# Split $1 into $prefix_:$suffix_
|
|
splitcolon()
|
|
{
|
|
prefix_=${1%%:*}
|
|
suffix_=${1#*:}
|
|
}
|
|
|
|
# Fetch repo $1, file $2
|
|
GET()
|
|
{
|
|
if isurl ssh "$1"
|
|
then
|
|
splitcolon "${1#ssh://}"
|
|
(exec 0>&-; ssh "$prefix_" "cat $suffix_/$2")
|
|
elif isurl sftp "$1"
|
|
then
|
|
(exec 0>&-; curl -s -S -k "$1/$2")
|
|
else
|
|
cat "$1/$2"
|
|
fi
|
|
}
|
|
|
|
# Fetch repo $1, file $2 or return encrypted empty message
|
|
GET_OR_EMPTY() { GET "$@" 2>/dev/null || (printf "" | ENCRYPT) ; }
|
|
|
|
# Put repo $1, file $2 or fail
|
|
PUT()
|
|
{
|
|
if isurl ssh "$1"
|
|
then
|
|
splitcolon "${1#ssh://}"
|
|
ssh "$prefix_" "cat > $suffix_/$2"
|
|
elif isurl sftp "$1"
|
|
then
|
|
curl -s -S -k --ftp-create-dirs -T - "$1/$2"
|
|
else
|
|
cat > "$1/$2"
|
|
fi
|
|
}
|
|
|
|
# Put directory for repo $1
|
|
PUTREPO()
|
|
{
|
|
if isurl ssh "$1"
|
|
then
|
|
splitcolon "${1#ssh://}"
|
|
(exec 0>&- ; ssh "$prefix_" "mkdir -p $suffix_")
|
|
elif isurl sftp "$1"
|
|
then
|
|
:
|
|
else
|
|
mkdir -p "$1"
|
|
fi
|
|
}
|
|
|
|
ENCRYPT()
|
|
{
|
|
# Security protocol:
|
|
# Symmetric encryption using the long MASTERKEY. We use AES
|
|
# and OpenPGP's modification detection code (mdc).
|
|
(printf "%s" "$MASTERKEY" | \
|
|
gpg --batch --force-mdc --compress-algo none \
|
|
--passphrase-fd 0 --output - -c /dev/fd/3) 3<&0
|
|
}
|
|
|
|
CLEARSIGN()
|
|
{
|
|
if [ "$CONF_SIGN_MANIFEST" = "true" ]
|
|
then
|
|
echo_info "Signing new manifest"
|
|
gpg --output - --clearsign
|
|
else
|
|
cat
|
|
fi
|
|
}
|
|
|
|
DECRYPT()
|
|
{
|
|
(printf "%s" "$MASTERKEY" | \
|
|
gpg -q --batch --passphrase-fd 0 --output - -d /dev/fd/3) 3<&0
|
|
}
|
|
|
|
# Append $2 to $1 with a newline separator
|
|
append()
|
|
{
|
|
[ -n "$1" ] && printf "%s\n" "$1" || :
|
|
printf "%s\n" "$2"
|
|
}
|
|
|
|
tac() { sed '1!G;h;$!d'; }
|
|
echo_info() { echo "gcrypt:" "$@" >&2; }
|
|
|
|
make_new_repo()
|
|
{
|
|
# Security protocol:
|
|
# The MASTERKEY is encrypted to all RECIPIENTS. The key is a long
|
|
# ascii-encoded string used for symmetric encryption with GnuPG.
|
|
local RECIPIENTS
|
|
echo_info "Setting up new repository at $URL"
|
|
# Split recipients by space, example "a b c" => -R a -R b -R c
|
|
RECIPIENTS=$(git config gcrypt.recipients | sed -e 's/\([^ ]\+\)/-R &/g')
|
|
if [ -z "$RECIPIENTS" ]
|
|
then
|
|
echo_info "You must configure which GnuPG recipients can access the repository."
|
|
echo_info "To setup for all your git repositories, use::"
|
|
echo_info " git config --global gcrypt.recipients KEYID"
|
|
exit 1
|
|
fi
|
|
PUTREPO "$URL"
|
|
echo_info "Generating new master key"
|
|
MASTERKEY="$(genkey)"
|
|
printf "%s" "$MASTERKEY" | \
|
|
gpg --compress-algo none -e $RECIPIENTS | PUT "$URL" masterkey
|
|
}
|
|
|
|
read_config()
|
|
{
|
|
CONF_SIGN_MANIFEST=$(git config --bool gcrypt.signmanifest || :)
|
|
CONF_REQUIRE_SIGN=$(git config --bool gcrypt.requiresign || :)
|
|
}
|
|
|
|
ensure_connected()
|
|
{
|
|
local MANIFESTDATA
|
|
local STRIPDATA
|
|
|
|
if [ -n "$DID_FIND_REPO" ]
|
|
then
|
|
return
|
|
fi
|
|
DID_FIND_REPO=yes
|
|
read_config
|
|
|
|
MASTERKEY="$(get_masterkey)"
|
|
if [ -z "$MASTERKEY" ]
|
|
then
|
|
DID_FIND_REPO=no
|
|
return
|
|
fi
|
|
MANIFESTDATA="$(GET_OR_EMPTY "$URL" manifest | DECRYPT)"
|
|
if [ -n "$MANIFESTDATA" -a \( "$CONF_REQUIRE_SIGN" = true -o \
|
|
-z "${MANIFESTDATA##-----BEGIN*}" \) ]
|
|
then
|
|
# Use gpg to verify and strip the signature
|
|
echo_info "Verifying manifest signature"
|
|
STRIPDATA=$(printf "%s" "$MANIFESTDATA" | gpg || {
|
|
echo_info "WARNING: Failed to verify signature from $URL"
|
|
[ "$CONF_REQUIRE_SIGN" = "true" ] && \
|
|
echo_info "Exiting per gcrypt.requiresign" && exit 1
|
|
}
|
|
)
|
|
[ -n "$STRIPDATA" ] && MANIFESTDATA=$STRIPDATA
|
|
fi
|
|
BRANCHLIST=$(printf "%s\n" "$MANIFESTDATA" | (grep -E '^[0-9a-f]{40}' || :))
|
|
PACKLIST=$(printf "%s\n" "$MANIFESTDATA" | (grep "^$PACKPFX" || :))
|
|
}
|
|
|
|
get_masterkey()
|
|
{
|
|
GET "$URL" masterkey 2>/dev/null | gpg -q --batch -d || :
|
|
}
|
|
|
|
do_capabilities()
|
|
{
|
|
echo fetch
|
|
echo push
|
|
echo
|
|
}
|
|
|
|
do_list()
|
|
{
|
|
local OBJID
|
|
local REFNAME
|
|
ensure_connected
|
|
|
|
printf "%s\n" "$BRANCHLIST" | while read LINE
|
|
do
|
|
[ -z "$LINE" ] && break
|
|
OBJID=${LINE%% *}
|
|
REFNAME=${LINE##* }
|
|
echo "$OBJID" "$REFNAME"
|
|
if [ "$REFNAME" = "refs/heads/master" ]
|
|
then
|
|
echo "@refs/heads/master HEAD"
|
|
fi
|
|
done
|
|
|
|
# end with blank line
|
|
echo
|
|
}
|
|
|
|
do_fetch()
|
|
{
|
|
# Security protocol:
|
|
# The PACK id is the SHA-1 of the encrypted git packfile.
|
|
# We only download packs mentioned in the encrypted manifest,
|
|
# and check their digest when received.
|
|
local PNEED
|
|
local PBOTH
|
|
local PHAVE
|
|
ensure_connected
|
|
|
|
if [ -z "$PACKLIST" ]
|
|
then
|
|
echo # end with blank line
|
|
return
|
|
fi
|
|
|
|
TMPPACK_ENCRYPTED="$LOCALDIR/tmp_pack_ENCRYPTED_.$$"
|
|
trap 'rm -f "$TMPPACK_ENCRYPTED"' EXIT
|
|
|
|
# Needed packs is REMOTE - (HAVE & REMOTE)
|
|
PHAVE="$(cat "$LOCALDIR/have_packs" 2>/dev/null || :)"
|
|
PBOTH="$(printf "%s\n%s" "$PACKLIST" "$PHAVE" | sort | uniq -d)"
|
|
PNEED="$(printf "%s\n%s" "$PACKLIST" "$PBOTH" | sort | uniq -u)"
|
|
|
|
printf "%s\n" "$PNEED" | while read PACKLINE
|
|
do
|
|
PACK=${PACKLINE#"$PACKPFX"}
|
|
RCVID="$(GET "$URL" "$PACK" | tee "$TMPPACK_ENCRYPTED" | pack_hash)"
|
|
if [ "$RCVID" != "$PACK" ]
|
|
then
|
|
echo_info "Packfile $PACK does not match digest!"
|
|
exit 1
|
|
fi
|
|
DECRYPT < "$TMPPACK_ENCRYPTED" | git unpack-objects
|
|
|
|
# add to local pack list
|
|
printf "$PACKPFX%s\n" "$PACK" >> "$LOCALDIR/have_packs"
|
|
done
|
|
|
|
rm -f "$TMPPACK_ENCRYPTED"
|
|
trap EXIT
|
|
echo # end with blank line
|
|
}
|
|
|
|
# do_push PUSHARGS (multiple lines like +src:dst, with both + and src opt.)
|
|
do_push()
|
|
{
|
|
# Security protocol:
|
|
# Each git packfile is encrypted and then named for the encrypted
|
|
# file's hash. The manifest is updated with the pack id.
|
|
# The manifest is encrypted.
|
|
local REMOTEHAS
|
|
local REMOTEWANT
|
|
local prefix_
|
|
local suffix_
|
|
ensure_connected
|
|
|
|
if [ "$DID_FIND_REPO" = "no" ]
|
|
then
|
|
make_new_repo
|
|
fi
|
|
|
|
trap 'rm -f "$TMPMANIFEST" "$TMPPACK_ENCRYPTED" "$TMPOBJLIST"' EXIT
|
|
TMPMANIFEST="$LOCALDIR/tmp_new_manifest_.$$"
|
|
touch "$TMPMANIFEST"
|
|
if [ -n "$BRANCHLIST" ]
|
|
then
|
|
printf "%s\n" "$BRANCHLIST" >"$TMPMANIFEST"
|
|
REMOTEHAS="$(printf "%s" "$BRANCHLIST" | \
|
|
cut -f1 -d' ' | sed -e s/^/^/ | tr '\n' ' ')"
|
|
fi
|
|
|
|
REMOTEWANT="$(printf "%s\n" "$1" | while read LINE
|
|
do
|
|
# +src:dst -- remove leading + then split at :
|
|
splitcolon "${LINE#+}"
|
|
if [ -n "$prefix_" ]
|
|
then
|
|
printf "%s " "$prefix_"
|
|
printf "%s %s\n" "$(git rev-parse "$prefix_")" "$suffix_" >> "$TMPMANIFEST"
|
|
# else delete
|
|
fi
|
|
done)"
|
|
|
|
# POSIX compat issue: sort -s (stable), but supported in bsd and gnu
|
|
BRANCHLIST="$(sort -k2 -s "$TMPMANIFEST" | tac | uniq -s40)"
|
|
|
|
TMPPACK_ENCRYPTED="$LOCALDIR/tmp_pack_ENCRYPTED_.$$"
|
|
TMPOBJLIST="$LOCALDIR/tmp_packrevlist.$$"
|
|
git rev-list --objects $REMOTEHAS $REMOTEWANT -- | \
|
|
tee "$TMPOBJLIST" | \
|
|
git pack-objects --stdout | ENCRYPT > "$TMPPACK_ENCRYPTED"
|
|
# Only send pack if we have any objects to send
|
|
if [ -s "$TMPOBJLIST" ]
|
|
then
|
|
PACKID=$(pack_hash < "$TMPPACK_ENCRYPTED")
|
|
PACKLIST=$(append "$PACKLIST" "$PACKPFX$PACKID")
|
|
PUT "$URL" "$PACKID" < "$TMPPACK_ENCRYPTED"
|
|
fi
|
|
|
|
# Put new manifest
|
|
printf "%s\n%s\n" "$BRANCHLIST" "$PACKLIST" | \
|
|
CLEARSIGN | ENCRYPT | PUT "$URL" "manifest"
|
|
|
|
# ok all updates (not deletes)
|
|
printf "%s\n" "$1" | while read LINE
|
|
do
|
|
# +src:dst -- remove leading + then split at :
|
|
splitcolon "${LINE#+}"
|
|
if [ -z "$prefix_" ]
|
|
then
|
|
echo "error $suffix_ delete not supported yet"
|
|
else
|
|
echo "ok $suffix_"
|
|
fi
|
|
done
|
|
|
|
rm -f "$TMPPACK_ENCRYPTED"
|
|
rm -f "$TMPMANIFEST"
|
|
rm -f "$TMPOBJLIST"
|
|
trap EXIT
|
|
echo
|
|
}
|
|
|
|
# Main program, check $URL is supported
|
|
NAME=$1
|
|
URL=$2
|
|
( isurl ssh "$URL" || isurl sftp "$URL" || test -z ${URL##/*} ) || \
|
|
{ echo_info "Supported URLs: Absolute path, sftp://, ssh://" ; exit 1 ; }
|
|
|
|
mkdir -p "$LOCALDIR"
|
|
|
|
while read INPUT
|
|
do
|
|
#echo_info "Got: $INPUT"
|
|
case "$INPUT" in
|
|
capabilities)
|
|
do_capabilities
|
|
;;
|
|
list|list\ for-push)
|
|
do_list
|
|
;;
|
|
fetch\ *)
|
|
FETCH_ARGS="${INPUT##fetch }"
|
|
while read INPUTX
|
|
do
|
|
case "$INPUTX" in
|
|
fetch*)
|
|
FETCH_ARGS= #ignored
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
do_fetch "$FETCH_ARGS"
|
|
;;
|
|
push\ *)
|
|
PUSH_ARGS="${INPUT##push }"
|
|
while read INPUTX
|
|
do
|
|
#echo_info "Got: (for push) $INPUTX"
|
|
case "$INPUTX" in
|
|
push\ *)
|
|
PUSH_ARGS=$(append "$PUSH_ARGS" "${INPUTX#push }")
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
do_push "$PUSH_ARGS"
|
|
;;
|
|
?*)
|
|
echo_info "Unknown input!"
|
|
exit 1
|
|
;;
|
|
*)
|
|
#echo_info "Blank line, we are done"
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|