git-remote-gcrypt/git-remote-gcrypt

526 lines
12 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
genkey()
{
gpg --armor --gen-rand 1 128 | tr -d \\n
}
pack_hash()
{
local HASH=$(gpg --with-colons --print-md SHA224 | tr A-F a-f)
HASH=${HASH#:*:}; printf "%s" "${HASH%:}"
}
LOCALDIR="${GIT_DIR:-.git}/remote-gcrypt"
DID_FIND_REPO= # yes for connected, no for no repo
PACKPFX="pack :SHA224:"
export GITCEPTION="$GITCEPTION+" # Reuse $GREF except when stacked
GREF="refs/gcrypt/gitception.$GITCEPTION"
isurl() { test -z "${2%%$1://*}" ; }
# Split $1 into $prefix_:$suffix_
splitcolon()
{
prefix_=${1%%:*}
suffix_=${1#*:}
}
## gitception part
# Fetch giturl $1, file $2
gitception_get()
{
# Take care to preserve FETCH_HEAD
local FHEAD
local RETVAL
FHEAD="$GIT_DIR/FETCH_HEAD"
[ -e "$FHEAD" ] && command mv -f "$FHEAD" "$FHEAD.$$~" || :
git fetch -q -f "$1" HEAD:"$GREF" 2>/dev/tty >/dev/null &&
OBJID="$(git ls-tree "$GREF" |
xgrep -E "\b$2$" | awk '{print $3}')" &&
[ -n "$OBJID" ] && git cat-file blob "$OBJID" && RETVAL=: ||
{ RETVAL=false && : ; }
[ -e "$FHEAD.$$~" ] && command mv -f "$FHEAD.$$~" "$FHEAD" || :
$RETVAL
}
anon_commit()
{
GIT_AUTHOR_NAME="root" GIT_AUTHOR_EMAIL="root@localhost" \
GIT_AUTHOR_DATE="1356994801 -0400" GIT_COMMITTER_NAME="root" \
GIT_COMMITTER_EMAIL="root@localhost" \
GIT_COMMITTER_DATE="1356994801 -0400" \
git commit-tree "$@"
}
# Get 'tree' from $1, change file $2 to obj id $3
update_tree()
{
# $2 is a filename from the repo format
(git ls-tree "$1" | xgrep -v -E "\b$2$";
printf "100644 blob %s\t%s" "$3" "$2") | git mktree
}
# Put giturl $1, file $2
# depends on previous GET to set $GREF and depends on PUT_FINAL later
gitception_put()
{
OBJID=$(git hash-object -w --stdin) && \
TREEID=$(update_tree "$GREF" "$2" "$OBJID") &&
COMMITID=$(anon_commit "$TREEID" -m "x") && \
git update-ref "$GREF" "$COMMITID"
}
## end gitception
# 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")
elif isurl gitception "$1"
then
gitception_get "${1#gitception://}" "$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"
elif isurl gitception "$1"
then
gitception_put "${1#gitception://}" "$2"
else
cat > "$1/$2"
fi
}
# Put all PUT changes for repo $1 at once
PUT_FINAL()
{
if isurl gitception "$1"
then
git push --quiet -f "${1#gitception://}" "$GREF":master
else
:
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
:
elif isurl gitception "$1"
then
# FIXME
:
else
mkdir -p "$1"
fi
}
CLEAN_FINAL()
{
isurl gitception "$1" && git update-ref -d "$GREF" || :
}
ENCRYPT()
{
# Security protocol:
# Symmetric encryption using the long MASTERKEY.
(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 "Requesting manifest signature for push"
gpg --output - --clearsign
else
cat
fi
}
CHECKSIGN()
{
gpg -q --batch --no-default-keyring \
--secret-keyring /dev/null --keyring "$CONF_KEYRING" -d
}
DECRYPT()
{
(printf "%s" "$MASTERKEY" | \
gpg -q --batch --no-default-keyring --secret-keyring /dev/null \
--keyring /dev/null \
--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"
}
xgrep() { command grep "$@" || : ; }
sort_C() { LC_ALL=C command sort "$@"; }
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
local KEYSIGN
echo_info "Setting up new repository at $URL"
RECIPIENTS="$(gpg --no-default-keyring --keyring "$CONF_KEYRING" \
--with-colons -k | xgrep ^pub | cut -f5 -d:)"
# Split recipients by space, example "a b c" => -R a -R b -R c
RECIPIENTS=$(printf "%s" $RECIPIENTS | sed -e 's/\([^ ]\+\)/-R &/g')
if [ -z "$RECIPIENTS" ]
then
echo_info "You must configure a keyring for the repository."
echo_info "Use ::"
echo_info " gpg --export KEYID1 > <path-to-keyring>"
echo_info " git config gcrypt.keyring <path-to-keyring>"
exit 1
fi
PUTREPO "$URL"
echo_info "Generating master key"
echo_info "Requesting master key signature"
MASTERKEY="$(genkey)"
KEYSIGN=$(printf "%s\n" "$MASTERKEY" | gpg --output - --clearsign)
TMPMASTERKEY_ENC="$LOCALDIR/masterenc.$$"
trap 'rm -f "$TMPMASTERKEY_ENC"' EXIT
echo_info "Encrypting masterkey to \"$RECIPIENTS\""
printf "%s" "$KEYSIGN" | gpg --batch --no-default-keyring \
--secret-keyring /dev/null --keyring "$CONF_KEYRING" \
--compress-algo none -e $RECIPIENTS > "$TMPMASTERKEY_ENC"
PUT "$URL" masterkey < "$TMPMASTERKEY_ENC"
rm -f "$TMPMASTERKEY_ENC"
trap EXIT
}
get_masterkey()
{
TMPMASTERKEY_ENC="$LOCALDIR/masterenc.$$"
trap 'rm -f "$TMPMASTERKEY_ENC"' EXIT
GET "$URL" masterkey 2>/dev/null > "$TMPMASTERKEY_ENC" || return 0
echo_info "Verifying master key signature"
gpg -q -d < "$TMPMASTERKEY_ENC" | CHECKSIGN || {
echo_info "Opening of master key failed!"
echo_info "Using keyring $CONF_KEYRING"
if [ "$CONF_KEYRING" = "/dev/null" ] ; then
echo_info "Please configure gcrypt.keyring"
fi
exit 1
}
rm -f "$TMPMASTERKEY_ENC"
trap EXIT
}
read_config()
{
CONF_SIGN_MANIFEST=$(git config --bool gcrypt.signmanifest || :)
CONF_REQUIRE_SIGN=$(git config --bool gcrypt.requiresign || :)
CONF_KEYRING=$(git config --path gcrypt.keyring || printf "/dev/null")
}
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 "$URL" manifest | DECRYPT)"
if [ "$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" | CHECKSIGN || {
echo_info "WARNING: Failed to verify signature from $URL"
echo_info "WARNING: Using keyring $CONF_KEYRING"
if [ "$CONF_KEYRING" = "/dev/null" ] ; then
echo_info "WARNING: Please configure gcrypt.keyring"
fi
if [ "$CONF_REQUIRE_SIGN" = "true" ] ; then
echo_info "Exiting per gcrypt.requiresign" && exit 1
fi
}
)"
[ -n "$STRIPDATA" ] && MANIFESTDATA=$STRIPDATA || :
fi
[ -n "$MANIFESTDATA" ] || exit 1
BRANCHLIST=$(printf "%s\n" "$MANIFESTDATA" | xgrep -E '^[0-9a-f]{40}')
PACKLIST=$(printf "%s\n" "$MANIFESTDATA" | xgrep "^$PACKPFX")
}
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)
# The `+` for $GITCEPTION is pointless but we will be safe for stacking
PHAVE="$(cat "$LOCALDIR/have_packs+" 2>/dev/null || :)"
PBOTH="$(printf "%s\n%s" "$PACKLIST" "$PHAVE" | sort_C | uniq -d)"
PNEED="$(printf "%s\n%s" "$PACKLIST" "$PBOTH" | sort_C | uniq -u)"
printf "%s\n" "$PNEED" | while read PACKLINE
do
[ -z "$PACKLINE" ] && break
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 index-pack -v --stdin >/dev/null
# add to local pack list
printf "$PACKPFX%s\n" "$PACK">>"$LOCALDIR/have_packs$GITCEPTION"
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 SIGNMANIFEST
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_C -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
SIGNMANIFEST=$(printf "%s\n%s\n" "$BRANCHLIST" "$PACKLIST" | CLEARSIGN)
printf "%s\n" "$SIGNMANIFEST" | ENCRYPT | PUT "$URL" "manifest"
PUT_FINAL "$URL"
# 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" || isurl gitception "$URL" || \
test -z ${URL##/*} ) || \
{ echo_info "Supported URLs: gitception://<giturl>, Absolute path, sftp://, ssh://" ; exit 1 ; }
mkdir -p "$LOCALDIR"
while read INPUT
do
#echo_info "Got: $INPUT ($GITCEPTION)"
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"
CLEAN_FINAL "$URL"
exit 0
;;
esac
done