git-remote-gcrypt/git-remote-gcrypt
root 4f04d2d43b Encrypt to self by default; basic functionality now needs no configuration
Introduces gcrypt.participants "simple" mode which encrypts to self, and
accepts any valid signature by default. No configuration needed for
private repositories.

We also adds  remote.<name>.gcrypt-participants to configure this per
remote.
2013-02-14 00:00:00 +00:00

774 lines
17 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"
Gref_rbranch="refs/heads/master"
Repoid=
Packkey_bytes=33 # 33 random bytes for passphrase, still compatible if changed
Hashtype=SHA256 # SHA512 SHA384 SHA256 SHA224 supported.
Packpat="pack :*:"
Manifestfile=91bd0c092128cf2e60e1a608c31e92caf1f9c1595f83f2890ef17c0e4881aa0a
Urlfrag=
Branchlist=
Packlist=
Keeplist=
Extension_list=
Repack_limit=25
Packlist_delete=
Recipients=
Signers=
Goodsig=
# compat/utility functions
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; }
isnull() { case "$1" in "") return 0;; *) return 1;; esac; }
isnonnull() { ! isnull "$1"; }
iseq() { case "$1" in "$2") return 0;; *) return 1;; esac; }
isnoteq() { ! iseq "$@"; }
# Append $2 to $1 with a newline separator
append() { isnull "$1" || xecho "$1" && xecho "$2"; }
isurl() { isnull "${2%%$1://*}"; }
islocalrepo() { isnull "${1##/*}" && [ ! -e "$1/HEAD" ]; }
xgrep() { command grep "$@" || : ; }
sort_C() { LC_ALL=C command sort "$@"; }
sort_stable_k2()
{
awk '{ printf("%08d\t%s\n", NR, $0) }' | sort_C -k 3,3 -k 1,1 |cut -f 2-
}
tac() { sed '1!G;h;$!d'; }
# Split $1 into $prefix_:$suffix_
splitcolon()
{
prefix_=${1%%:*}
suffix_=${1#*:}
}
repoidstr() { xecho "repo $Repoid"; }
## 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" "refs/heads/${Urlfrag:-master}:$Gref" 2>/dev/tty >/dev/null &&
obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" &&
isnonnull "$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 "$@" <<EOF
Initial commit
EOF
}
# 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") &&
git update-ref "$Gref" "$commit_id"
}
# Remove giturl $1, file $2
# depends on previous GET like put
gitception_remove()
{
local tree_id= commit_id= tab_=" "
# $2 is a filename from the repo format
tree_id=$(git ls-tree "$Gref" | xgrep -v -E '\b'"$2"'$' | git mktree) &&
commit_id=$(anon_commit "$tree_id") &&
git update-ref "$Gref" "$commit_id"
}
gitception_new_repo()
{
local empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904
# get any file to update Gref, and if it's not updated we create empty
git update-ref -d "$Gref" || :
gitception_get "$1" "x" || :
git rev-parse -q --verify "$Gref" >/dev/null && return 0 ||
commit_id=$(anon_commit "$empty_tree") &&
git update-ref "$Gref" "$commit_id"
}
## end gitception
# Fetch repo $1, file $2, tmpfile in $3
GET()
{
if isurl sftp "$1"
then
(exec 0>&-; curl -s -S -k "$1/$2") > "$3"
elif isurl rsync "$1"
then
(exec 0>&-; rsync -I -W "${1#rsync://}"/"$2" "$3" >&2)
elif islocalrepo "$1"
then
cat "$1/$2" > "$3"
else
gitception_get "${1#gitception://}" "$2" > "$3"
fi
}
# Put repo $1, file $2 or fail, tmpfile in $3
PUT()
{
if isurl sftp "$1"
then
curl -s -S -k --ftp-create-dirs -T "$3" "$1/$2"
elif isurl rsync "$1"
then
rsync -I -W "$3" "${1#rsync://}"/"$2" >&2
elif islocalrepo "$1"
then
cat > "$1/$2" < "$3"
else
gitception_put "${1#gitception://}" "$2" < "$3"
fi
}
# Put all PUT changes for repo $1 at once
PUT_FINAL()
{
if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1"
then
:
else
git push --quiet -f "${1#gitception://}" \
"$Gref:refs/heads/${Urlfrag:-master}"
fi
}
# Put directory for repo $1
PUTREPO()
{
if isurl sftp "$1"
then
:
elif isurl rsync "$1"
then
rsync -q -r --exclude='*' "$Localdir/" "${1#rsync://}" >&2
elif islocalrepo "$1"
then
mkdir -p "$1"
else
gitception_new_repo "${1#gitception://}"
fi
}
# For repo $1, delete all newline-separated files in $2
REMOVE()
{
local fn_=
if isurl sftp "$1"
then
# FIXME
echo_info "sftp: Ignore remove request $1/$2"
elif isurl rsync "$1"
then
xecho "$2" | rsync -I -W -v -r --delete --include-from=- \
--exclude='*' "$Localdir"/ "${1#rsync://}/" >&2
elif islocalrepo "$1"
then
for fn_ in $2; do
rm -f "$1"/"$fn_"
done
else
for fn_ in $2; do
gitception_remove "${1#gitception://}" "$fn_"
done
fi
}
CLEAN_FINAL()
{
if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1"
then
:
else
git update-ref -d "$Gref" || :
fi
}
addsignkeyparam()
{
if isnull "$Conf_signkey"; then
"$@"
else
"$@" -u "$Conf_signkey"
fi
}
ENCRYPT()
{
gpg --batch --force-mdc --compress-algo none --passphrase-fd 3 -c 3<<EOF
$1
EOF
}
DECRYPT()
{
gpg -q --batch --no-default-keyring --secret-keyring /dev/null \
--keyring /dev/null --passphrase-fd 3 -d 3<<EOF
$1
EOF
}
# Encrypt to recipients $1
PRIVENCRYPT()
{
addsignkeyparam gpg --compress-algo none -se $1
}
PRIVDECRYPT()
{
local status_=
exec 4>&1 &&
status_=$(gpg --status-fd 3 -q -d 3>&1 1>&4) &&
xecho "$status_" | grep "^\[GNUPG:\] ENC_TO " >/dev/null &&
(xecho "$status_" | grep -e "$Goodsig" >/dev/null || {
echo_info "Failed to verify manifest signature!" &&
echo_info "Only accepting signatories: ${Signers:-(none)}" &&
return 1
})
}
# Generate $1 random bytes
genkey()
{
gpg --armor --gen-rand 1 "$1"
}
gpg_hash()
{
local hash_=
hash_=$(gpg --with-colons --print-md "$1" | tr A-F a-f)
hash_=${hash_#:*:}
xecho "${hash_%:}"
}
pack_hash() { gpg_hash "$Hashtype"; }
# Pass the branch/ref by pipe to git
safe_git_rev_parse()
{
git cat-file --batch-check 2>/dev/null |
xgrep -v "missing" | cut -f 1 -d ' '
}
make_new_repo()
{
local urlid_= fix_config=
echo_info "Setting up new repository"
PUTREPO "$URL"
# Needed assumption: the same user should have no duplicate Repoid
Repoid=":${Hashtype}:$(genkey 64 | pack_hash)"
iseq "${NAME#gcrypt::}" "$URL" || {
git config "remote.$NAME.gcrypt-id" "$Repoid"
fix_config=1
}
echo_info "Repository ID is $Repoid"
Extension_list=$(xecho "extn comment")
#isnull "$fix_config" || echo_info "(configuration for $NAME updated)"
}
read_config()
{
local recp_= key_line= cap_= conf_keyring= conf_part=
Conf_signkey=$(git config --path user.signingkey || :)
conf_keyring=$(git config --path gcrypt.keyring || :)
conf_part=$(git config --get "remote.$NAME.gcrypt-participants" '.+' ||
git config --get gcrypt.participants '.+' || :)
# Figure out which keys we should encrypt to or accept signatures from
if isnonnull "$conf_keyring" && isnull "$conf_part"
then
echo_info "WARNING: Setting gcrypt.keyring is deprecated," \
"use gcrypt.participants instead."
conf_part=$(gpg --no-default-keyring --keyring "$conf_keyring" \
--with-colons --fast-list -k | grep ^pub | cut -f 5 -d :)
fi
if isnull "$conf_part" || iseq "$conf_part" simple
then
Signers="(default keyring)"
Recipients="--throw-keyids --default-recipient-self"
Goodsig="^\[GNUPG:\] GOODSIG "
return 0
fi
for recp_ in $conf_part
do
key_line=$(gpg --with-colons --fast-list -k "$recp_" | xgrep ^pub)
keyid_=$(xecho "$key_line" | cut -f 5 -d :)
isnonnull "$keyid_" &&
Signers="$Signers $keyid_" &&
Goodsig=$(append "$Goodsig" "^\[GNUPG:\] GOODSIG $keyid_") || {
echo_info "WARNING: Skipping missing key $recp_"
continue
}
# Check 'E'ncrypt capability
cap_=$(xecho "$key_line" | cut -f 12 -d :)
iseq "${cap_#*E}" "$cap_" || Recipients="$Recipients -R $keyid_"
done
if isnull "$Recipients"
then
echo_info "You have not configured any keys to encrypt to for this repository"
echo_info "Use ::"
echo_info " git config gcrypt.participants YOURKEYID"
exit 1
fi
}
ensure_connected()
{
local manifest_= rcv_repoid= r_name=
if isnonnull "$Did_find_repo"
then
return
fi
Did_find_repo=no
read_config
iseq "${NAME#gcrypt::}" "$URL" || r_name=$NAME
# Fixup ssh:// -> rsync://
if isurl ssh "$URL"; then
URL="rsync://${URL#ssh://}"
fi
# Find the URL fragment
Urlfrag=${URL##*"#"}
isnoteq "$Urlfrag" "$URL" || Urlfrag=
URL=${URL%"#$Urlfrag"}
# manifestfile -- sha224 hash if we can, else the default location
if isurl sftp "$URL" || islocalrepo "$URL" || isurl rsync "$URL"
then
# not for gitception
isnull "$Urlfrag" || Manifestfile=$(xecho_n "$Urlfrag" | gpg_hash SHA224)
fi
Repoid=
isnull "$r_name" || {
Repoid=$(git config "remote.$r_name.gcrypt-id" || :)
}
TmpManifest_Enc="$Localdir/tmp_manifest.$$"
GET "$URL" "$Manifestfile" "$TmpManifest_Enc" 2>/dev/null || {
echo_info "Repository not found: $URL"
return 0
}
Did_find_repo=yes
echo_info "Decrypting manifest"
manifest_=$(PRIVDECRYPT < "$TmpManifest_Enc") &&
isnonnull "$manifest_" ||
echo_die "Failed to decrypt manifest!"
rm -f "$TmpManifest_Enc"
Branchlist=$(xecho "$manifest_" | xgrep -E '^[0-9a-f]{40} ')
Packlist=$(xecho "$manifest_" | xgrep "^pack ")
Keeplist=$(xecho "$manifest_" | xgrep "^keep ")
Extension_list=$(xecho "$manifest_" | xgrep "^extn ")
rcv_repoid=$(xecho "$manifest_" | xgrep "^repo ")
rcv_repoid=${rcv_repoid#repo }
rcv_repoid=${rcv_repoid% *}
if isnull "$Repoid"
then
echo_info "Remote repo ID is $rcv_repoid"
Repoid=$rcv_repoid
elif isnoteq "$rcv_repoid" "$Repoid"
then
echo_info "WARNING:"
echo_info "WARNING: Remote repository ID has changed!"
echo_info "WARNING: to $rcv_repoid"
echo_info "WARNING:"
Repoid=$rcv_repoid
else
return 0
fi
isnull "$r_name" || {
git config "remote.$r_name.gcrypt-id" "$rcv_repoid"
}
}
fetch_decrypt_pack()
{
local key_= rcv_id= htype_= pack_= hfunc_=
splitcolon "${1#pack :}"
htype_=$prefix_
pack_=$suffix_
if isnoteq "$htype_" SHA256 && isnoteq "$htype_" SHA224 &&
isnoteq "$htype_" SHA384 && isnoteq "$htype_" SHA512
then
echo_die "Packline malformed: $1"
fi
GET "$URL" "$pack_" "$TmpPack_Encrypted" &&
rcv_id=$(gpg_hash "$htype_" < "$TmpPack_Encrypted") &&
iseq "$rcv_id" "$pack_" ||
echo_die "Packfile $pack_ does not match digest!"
key_=$(xecho "$Packlist" | grep "$pack_" | cut -f 3 -d ' ')
DECRYPT "$key_" < "$TmpPack_Encrypted"
}
# $1 is new pack id $2 key
# set did_repack=yes if repacked
repack_if_needed()
{
local pack_= packline_= premote_= key_= pkeep_= n_=
# $TmpPack_Encrypted set in caller
did_repack=no
isnonnull "$Packlist" || return 0
if isnonnull "$GCRYPT_FULL_REPACK"
then
Keeplist=
Repack_limit=1
fi
premote_=$(xecho "$Packlist" | cut -f 1-2 -d ' ')
pkeep_=$(xecho "$Keeplist" | cut -f 2 -d ' ')
if isnull "$pkeep_"; then
n_=$(xecho "$Packlist" | wc -l)
else
n_=$(xecho "$Packlist" | grep -v -e "$pkeep_" | wc -l)
fi
if [ $Repack_limit -gt "$n_" ]; then
return
fi
echo_info "Repacking remote $NAME, ..."
rm -r -f "$Localdir/pack"
mkdir -p "$Localdir/pack"
DECRYPT "$2" < "$TmpPack_Encrypted" |
git index-pack -v --stdin "$Localdir/pack/${1}.pack" >/dev/null
xecho "$premote_" | while read packline_
do
isnonnull "$packline_" || continue
if isnonnull "$pkeep_" &&
xecho "$packline_" | grep -q -e "$pkeep_"
then
continue
fi
pack_=${packline_#$Packpat}
fetch_decrypt_pack "$packline_" |
git index-pack -v --stdin "$Localdir/pack/${pack_}.pack" >/dev/null
done
key_=$(genkey "$Packkey_bytes")
git verify-pack -v "$Localdir"/pack/*.idx | grep -E '^[0-9a-f]{40}' |
cut -f 1 -d ' ' |
GIT_ALTERNATE_OBJECT_DIRECTORIES=$Localdir \
git pack-objects --stdout | ENCRYPT "$key_" > "$TmpPack_Encrypted"
# Truncate packlist to only the kept packs
if isnull "$pkeep_"; then
Packlist_delete=$premote_
Packlist=
else
Packlist_delete=$(xecho "$premote_" | xgrep -v -e "$pkeep_")
Packlist=$(xecho "$Packlist" | xgrep -e "$pkeep_")
fi
pack_id=$(pack_hash < "$TmpPack_Encrypted")
Packlist=$(append "$Packlist" "pack :${Hashtype}:$pack_id $key_")
Keeplist=$(append "$Keeplist" "keep :${Hashtype}:$pack_id 1")
rm -r -f "$Localdir/pack"
did_repack=yes
}
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
isnonnull "$line_" || break
obj_id=${line_%% *}
ref_name=${line_##* }
echo_git "$obj_id" "$ref_name"
if iseq "$ref_name" "refs/heads/master"
then
echo_git "@refs/heads/master HEAD"
fi
done
# end with blank line
echo_git
}
do_fetch()
{
# The PACK id is the hash of the encrypted git packfile.
# We only download packs mentioned in the encrypted manifest,
# and check their digest when received.
local pack_= packline_= pneed_= phave_= premote_=
ensure_connected
if isnull "$Packlist"
then
echo_git # end with blank line
return
fi
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
premote_=$(xecho "$Packlist" | cut -f 1-2 -d ' ')
# The `+` for $GITCEPTION is pointless but we will be safe for stacking
phave_="$(cat "$Localdir/have_packs+" 2>/dev/null || :)"
pneed_="$(xecho "$premote_" | xgrep -v -x -e "$phave_")"
xecho "$pneed_" | while read packline_
do
isnonnull "$packline_" || continue
fetch_decrypt_pack "$packline_" |
git index-pack -v --stdin >/dev/null
# add to local pack list
xecho "${packline_}" >> "$Localdir/have_packs$GITCEPTION"
done
rm -f "$TmpPack_Encrypted"
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_= pack_id= key_=
del_hash=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ensure_connected
if iseq "$Did_find_repo" "no"
then
make_new_repo
fi
if isnonnull "$Branchlist"
then
# filter through batch-check to mark only the commits we have
remote_has=$(xecho "$Branchlist" | cut -f 1 -d ' ' |
safe_git_rev_parse | sed -e 's/^\(.\)/^&/')
fi
while read line_ # from <<
do
# +src:dst -- remove leading + then split at :
splitcolon "${line_#+}"
if isnonnull "$prefix_"
then
remote_want=$(append "$remote_want" "$prefix_")
Branchlist=$(append "$Branchlist" \
"$(xecho "$prefix_" | safe_git_rev_parse) $suffix_")
else
# Mark branch for deletion
Branchlist=$(append "$Branchlist" "$del_hash $suffix_")
fi
done <<EOF
$1
EOF
Branchlist=$(xecho "$Branchlist" | sort_stable_k2 | tac | uniq -s 40 |
xgrep -v "^$del_hash")
TmpPack_Encrypted="$Localdir/tmp_pack_ENCRYPTED_.$$"
TmpObjlist="$Localdir/tmp_packrevlist.$$"
key_=$(genkey "$Packkey_bytes")
append "$remote_has" "$remote_want" |
git rev-list --objects --stdin -- |
tee "$TmpObjlist" |
git pack-objects --stdout | ENCRYPT "$key_">"$TmpPack_Encrypted"
# Only send pack if we have any objects to send
if [ -s "$TmpObjlist" ]
then
pack_id=$(pack_hash < "$TmpPack_Encrypted")
did_repack=
repack_if_needed "$pack_id" "$key_"
if isnoteq "$did_repack" yes
then
Packlist=$(append "$Packlist" "pack :${Hashtype}:$pack_id $key_")
fi
# else, repack rewrote Packlist
fi
# Generate manifest
echo_info "Encrypting to: $Recipients"
echo_info "Requesting manifest signature"
TmpManifest_Enc="$Localdir/tmp_manifest.$$"
(xecho "$Branchlist"; xecho "$Packlist"; xecho "$Keeplist";
repoidstr; xecho "$Extension_list") |
PRIVENCRYPT "$Recipients" > "$TmpManifest_Enc"
# Upload pack
if [ -s "$TmpObjlist" ]
then
PUT "$URL" "$pack_id" "$TmpPack_Encrypted"
fi
rm -f "$TmpPack_Encrypted"
rm -f "$TmpObjlist"
# Upload manifest
PUT "$URL" "$Manifestfile" "$TmpManifest_Enc"
# Delete packs
if isnonnull "$Packlist_delete"; then
REMOVE "$URL" "$(xecho "$Packlist_delete" | while read packline_
do
isnonnull "$packline_" || continue
pack_=${packline_#$Packpat}
xecho "$pack_"
done)"
fi
PUT_FINAL "$URL"
rm -f "$TmpManifest_Enc"
# ok all updates
xecho "$1" | while read line_
do
# +src:dst -- remove leading + then split at :
splitcolon "${line_#+}"
echo_git "ok $suffix_"
done
echo_git
}
NAME=$1 # Remote name
URL=$2 # Remote URL
mkdir -p "$Localdir"
trap 'rm -f "$Localdir/tmp_"*".$$"' EXIT 1 2 3 15
echo_info "Development version -- Repository format WILL CHANGE in the future"
while read Input
do
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
case "$InputX" in
push\ *)
args_=$(append "$args_" "${InputX#push }")
;;
*)
break
;;
esac
done
do_push "$args_"
;;
?*)
echo_die "Unknown input!"
;;
*)
CLEAN_FINAL "$URL"
exit 0
;;
esac
done