git-remote-gcrypt/git-remote-gcrypt
root 71531be31e Replace use of printf and echo with a safe variant
Use  cat <<EOF  etc for safe output of all data to pipes (mostly that we
don't know what the shell does with echo and printf).
2013-02-14 00:00:00 +00:00

531 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
Did_find_repo= # yes for connected, no for no repo
Localdir="${GIT_DIR:-.git}/remote-gcrypt"
export GITCEPTION="$GITCEPTION+" # Reuse $Gref except when stacked
Gref="refs/gcrypt/gitception$GITCEPTION"
Repoid=
Packpfx="pack :SHA224:"
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 ret_=: obj_id= f_head="$GIT_DIR/FETCH_HEAD"
[ -e "$f_head" ] && command mv -f "$f_head" "$f_head.$$~" || :
git fetch -q -f "$1" HEAD:"$Gref" 2>/dev/tty >/dev/null &&
obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" &&
[ -n "$obj_id" ] && git cat-file blob "$obj_id" && ret_=: ||
{ ret_=false && : ; }
[ -e "$f_head.$$~" ] && command mv -f "$f_head.$$~" "$f_head" || :
$ret_
}
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()
{
local tab_=" "
# $2 is a filename from the repo format
(git ls-tree "$1" | xgrep -v -E '\b'"$2"'$';
xecho "100644 blob $3$tab_$2") | git mktree
}
# Put giturl $1, file $2
# depends on previous GET to set $Gref and depends on PUT_FINAL later
gitception_put()
{
local obj_id= tree_id= commit_id=
obj_id=$(git hash-object -w --stdin) &&
tree_id=$(update_tree "$Gref" "$2" "$obj_id") &&
commit_id=$(anon_commit "$tree_id" -m "x") &&
git update-ref "$Gref" "$commit_id"
}
## 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
}
# 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()
{
gpg --batch --force-mdc --compress-algo none \
--passphrase-fd 3 -c 3<<EOF
$Masterkey
EOF
}
DECRYPT()
{
gpg -q --batch --no-default-keyring --secret-keyring /dev/null \
--keyring /dev/null \
--passphrase-fd 3 -d 3<<EOF
$Masterkey
EOF
}
# Encrypt to recipients $1
PRIVENCRYPT()
{
gpg --no-default-keyring --keyring "$Conf_keyring" \
--compress-algo none -se $1
}
PRIVDECRYPT()
{
local status_=
exec 4>&1 &&
status_=$(gpg --no-default-keyring --keyring "$Conf_keyring" \
--status-fd 3 -q -d 3>&1 1>&4) &&
xecho "$status_" | grep "^\[GNUPG:\] ENC_TO " >/dev/null &&
(xecho "$status_" | grep "^\[GNUPG:\] GOODSIG " >/dev/null || {
echo_info "Failed to verify manifest signature!" && return 1
})
}
genkey()
{
gpg --armor --gen-rand 1 128 | tr -d \\n
}
pack_hash()
{
local hash_=
hash_=$(gpg --with-colons --print-md SHA224 | tr A-F a-f)
hash_=${hash_#:*:}
xecho "${hash_%:}"
}
# Append $2 to $1 with a newline separator
append()
{
[ -z "$1" ] || xecho "$1" && xecho "$2"
}
xgrep() { command grep "$@" || : ; }
sort_C() { LC_ALL=C command sort "$@"; }
tac() { sed '1!G;h;$!d'; }
xecho()
{
cat <<EOF
$@
EOF
}
xecho_n() { xecho "$@" | tr -d \\n ; } # kill newlines
echo_git() { xecho "$@" ; } # Code clarity
echo_info() { xecho "gcrypt:" "$@" >&2; }
echo_die() { echo_info "$@" ; exit 1; }
check_recipients()
{
Recipients="$(gpg --no-default-keyring --keyring "$Conf_keyring" \
--with-colons -k | xgrep ^pub | cut -f5 -d: | tr '\n' ' ')"
# Split recipients by space, example "a b c" => -R a -R b -R c
Recipients=$(xecho_n "$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
}
make_new_repo()
{
local urlid_= fix_config=
echo_info "Setting up new repository at $URL"
PUTREPO "$URL"
Masterkey="$(genkey)"
# We need a relatively short ID for URL+REPO
# The manifest will be stored at SHA224(urlid_)
# Needed assumption: the same user should have no duplicate urlid_
# For now, we use 20 random hex digits (80 bits), can be increased
urlid_=$(genkey | pack_hash | cut -c 1-20)
Repoid=$(xecho_n "$urlid_" | pack_hash)
echo_info "Repository ID is" "$urlid_"
[ "${NAME#gcrypt::}" != "$URL" ] && {
git config "remote.$NAME.url" "gcrypt::$URL/G/$urlid_"
fix_config=1
} || :
echo_info "Repository URL is" "gcrypt::$URL/G/$urlid_"
[ -n "$fix_config" ] && echo_info "(configuration for $NAME updated)"||:
}
read_config()
{
Conf_keyring=$(git config --path gcrypt.keyring || xecho "/dev/null")
}
ensure_connected()
{
local manifest_= rcv_repoid= url_id=
if [ -n "$Did_find_repo" ]
then
return
fi
Did_find_repo=no
read_config
# split out Repoid from URL
url_id=${URL##*/G/}
[ "$url_id" = "$URL" ] && url_id= && return 0 || :
URL=${URL%/G/"$url_id"}
Repoid=$(xecho_n "$url_id" | pack_hash)
TmpManifest_Enc="$Localdir/manifest.$$"
trap 'rm -f "$TmpManifest_Enc"' EXIT
GET "$URL" "$Repoid" 2>/dev/null > "$TmpManifest_Enc" ||
echo_die "Repository not found: $url_id at $URL"
Did_find_repo=yes
echo_info "Decrypting manifest"
manifest_=$(PRIVDECRYPT < "$TmpManifest_Enc") &&
[ "${#manifest_}" -gt 0 ] || {
echo_info "Failed to decrypt manifest!"
echo_info "Using keyring $Conf_keyring"
if [ "$Conf_keyring" = "/dev/null" ] ; then
echo_info "NOTE: Please configure gcrypt.keyring"
fi
exit 1
}
rm -f "$TmpManifest_Enc"
trap 0
Masterkey=$(xecho "$manifest_" | head -n 1)
Branchlist=$(xecho "$manifest_" | xgrep -E '^[0-9a-f]{40} ')
Packlist=$(xecho "$manifest_" | xgrep "^$Packpfx")
rcv_repoid=$(xecho "$manifest_" | xgrep "^repo ")
[ "repo $Repoid" = "$rcv_repoid" ] || echo_die "Repository id mismatch!"
}
do_capabilities()
{
echo_git fetch
echo_git push
echo_git
}
do_list()
{
local obj_id= ref_name= line_=
ensure_connected
xecho "$Branchlist" | while read line_
do
[ -z "$line_" ] && break
obj_id=${line_%% *}
ref_name=${line_##* }
echo_git "$obj_id" "$ref_name"
if [ "$ref_name" = "refs/heads/master" ]
then
echo_git "@refs/heads/master HEAD"
fi
done
# end with blank line
echo_git
}
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 pack_= rcv_id= packline_= pneed_= pboth_= phave_=
ensure_connected
if [ -z "$Packlist" ]
then
echo_git # end with blank line
return
fi
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
trap 'rm -f "$TmpPack_Encrypted"' EXIT
# Needed packs is Packlist - (phave & Packlist)
# The `+` for $GITCEPTION is pointless but we will be safe for stacking
phave_="$(cat "$Localdir/have_packs+" 2>/dev/null || :)"
pboth_="$( (xecho "$Packlist"; xecho "$phave_") | sort_C | uniq -d)"
pneed_="$( (xecho "$Packlist"; xecho "$pboth_") | sort_C | uniq -u)"
xecho "$pneed_" | while read packline_
do
[ -z "$packline_" ] && break
pack_=${packline_#"$Packpfx"}
rcv_id="$(GET "$URL" "$pack_" | \
tee "$TmpPack_Encrypted" | pack_hash)"
if [ "$rcv_id" != "$pack_" ]
then
echo_die "Packfile $pack_ does not match digest!"
fi
DECRYPT < "$TmpPack_Encrypted" |
git index-pack -v --stdin >/dev/null
# add to local pack list
xecho "$Packpfx$pack_" >> "$Localdir/have_packs$GITCEPTION"
done
rm -f "$TmpPack_Encrypted"
trap 0
echo_git # 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 remote_has= remote_want= prefix_= suffix_= line_= new_branch=
ensure_connected
check_recipients
if [ "$Did_find_repo" = "no" ]
then
make_new_repo
fi
trap 'rm -f "$TmpPack_Encrypted" "$TmpObjlist"' EXIT
if [ -n "$Branchlist" ]
then
remote_has=$(xecho "$Branchlist" |
cut -f1 -d' ' | sed -e 's/^\(.\)/^&/' | tr '\n' ' ')
fi
while read line_ # from <<
do
# +src:dst -- remove leading + then split at :
splitcolon "${line_#+}"
if [ -n "$prefix_" ]
then
remote_want="$remote_want$prefix_ "
Branchlist=$(append "$Branchlist" \
"$(git rev-parse "$prefix_") $suffix_")
else
: # FIXME delete branch
fi
done <<EOF
$1
EOF
# POSIX compat issue: sort -s (stable), but supported in bsd and gnu
Branchlist=$(xecho "$Branchlist" | sort_C -k2 -s | tac | uniq -s40)
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
TmpObjlist="$Localdir/tmp_packrevlist.$$"
git rev-list --objects $remote_has $remote_want -- | \
tee "$TmpObjlist" | \
git pack-objects --stdout | ENCRYPT > "$TmpPack_Encrypted"
# Only send pack if we have any objects to send
if [ -s "$TmpObjlist" ]
then
pack_id=$(pack_hash < "$TmpPack_Encrypted")
Packlist=$(append "$Packlist" "$Packpfx$pack_id")
PUT "$URL" "$pack_id" < "$TmpPack_Encrypted"
fi
rm -f "$TmpPack_Encrypted"
rm -f "$TmpObjlist"
trap 0
# Update manifest
echo_info "Encrypting manifest to \"$Recipients\""
echo_info "Requesting manifest key signature"
TmpManifest_Enc="$Localdir/manifest.$$"
trap 'rm -f "$TmpManifest_Enc"' EXIT
(xecho "$Masterkey"
xecho "$Branchlist"
xecho "$Packlist"
xecho "repo $Repoid") |
PRIVENCRYPT "$Recipients" > "$TmpManifest_Enc"
PUT "$URL" "$Repoid" < "$TmpManifest_Enc"
PUT_FINAL "$URL"
rm -f "$TmpManifest_Enc"
trap 0
# ok all updates (not deletes)
xecho "$1" | while read line_
do
# +src:dst -- remove leading + then split at :
splitcolon "${line_#+}"
if [ -z "$prefix_" ]
then
echo_git "error $suffix_ delete not supported yet"
else
echo_git "ok $suffix_"
fi
done
echo_git
}
# Main program, check $URL is supported
NAME=$1
URL=$2
( isurl ssh "$URL" || isurl sftp "$URL" ||
isurl gitception "$URL" || test -z ${URL##/*} ) ||
echo_die "Supported URLs: gitception://<giturl>, Absolute path, sftp://, ssh://"
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\ *)
args_="${Input##fetch }"
while read InputX
do
case "$InputX" in
fetch*)
args_= #ignored
;;
*)
break
;;
esac
done
do_fetch "$args_"
;;
push\ *)
args_="${Input##push }"
while read InputX
do
#echo_info "Got: (for push) $InputX"
case "$InputX" in
push\ *)
args_=$(append "$args_" "${InputX#push }")
;;
*)
break
;;
esac
done
do_push "$args_"
;;
?*)
echo_die "Unknown input!"
;;
*)
#echo_info "Blank line, we are done"
CLEAN_FINAL "$URL"
exit 0
;;
esac
done