#!/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=SHA224 # incompatible if changed Packpfx="pack :${Hashtype}:" Keeppfx="keep :${Hashtype}:" Branchlist= Packlist= Keeplist= Extension_list= Repack_limit=25 Packlist_delete= Recipients= Signers= Goodsig= # compat/utility functions xecho() { cat <&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 :${Hashtype}:$Repoid 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" "$Gref_rbranch:$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 "$@" </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:$Gref_rbranch" 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<&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" } pack_hash() { local hash_= hash_=$(gpg --with-colons --print-md "$Hashtype" | tr A-F a-f) hash_=${hash_#:*:} xecho "${hash_%:}" } # 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" # We need a relatively short ID for URL+REPO # The manifest will be stored at pack_hash($urlid_) # Needed assumption: the same user should have no duplicate urlid_ # For now, we arbitrarily use 9 random bytes (72 bits) urlid_=$(genkey 9 | tr '+/' '-_') Repoid=$(xecho_n "$urlid_" | pack_hash) iseq "${NAME#gcrypt::}" "$URL" || { git config "remote.$NAME.url" "gcrypt::$URL#$urlid_" fix_config=1 } echo_info "Repository URL is" "gcrypt::$URL#$urlid_" 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 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 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= url_id= if isnonnull "$Did_find_repo" then return fi Did_find_repo=no read_config # Fixup ssh:// -> rsync:// if isurl ssh "$URL"; then URL="rsync://${URL#ssh://}" fi # split out Repo ID from URL url_id=${URL##*"#"} isnoteq "$url_id" "$URL" || { url_id=${URL##*/"G."} isnoteq "$url_id" "$URL" || return 0 URL=${URL%/"G.$url_id"} } URL=${URL%"#$url_id"} Repoid=$(xecho_n "$url_id" | pack_hash) TmpManifest_Enc="$Localdir/tmp_manifest.$$" GET "$URL" "$Repoid" "$TmpManifest_Enc" 2>/dev/null || echo_die "Repository not found: $url_id at $URL" 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 "^$Packpfx") Keeplist=$(xecho "$manifest_" | xgrep "^keep") Extension_list=$(xecho "$manifest_" | xgrep "^extn ") rcv_repoid=$(xecho "$manifest_" | xgrep "^repo ") iseq "$(repoidstr)" "$rcv_repoid" || echo_die "Repository id mismatch!" } fetch_decrypt_pack() { local key_= rcv_id= GET "$URL" "$1" "$TmpPack_Encrypted" && rcv_id=$(pack_hash < "$TmpPack_Encrypted") && iseq "$rcv_id" "$1" || echo_die "Packfile $1 does not match digest!" key_=$(xecho "$Packlist" | grep "$1" | 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 -F -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 -F -e "$pkeep_" then continue fi pack_=${packline_#"$Packpfx"} fetch_decrypt_pack "$pack_" | 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 -F -e "$pkeep_") Packlist=$(xecho "$Packlist" | xgrep -F -e "$pkeep_") fi pack_id=$(pack_hash < "$TmpPack_Encrypted") Packlist=$(append "$Packlist" "$Packpfx$pack_id $key_") Keeplist=$(append "$Keeplist" "$Keeppfx$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 -F -vx -e "$phave_")" xecho "$pneed_" | while read packline_ do isnonnull "$packline_" || continue pack_=${packline_#"$Packpfx"} fetch_decrypt_pack "$pack_" | git index-pack -v --stdin >/dev/null # add to local pack list xecho "$Packpfx$pack_" >> "$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 <"$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" "$Packpfx$pack_id $key_") fi # else, repack rewrote Packlist fi # Generate manifest echo_info "Participants are: $Signers" 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" "$Repoid" "$TmpManifest_Enc" # Delete packs if isnonnull "$Packlist_delete"; then REMOVE "$URL" "$(xecho "$Packlist_delete" | while read packline_ do isnonnull "$packline_" || continue pack_=${packline_#"$Packpfx"} 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 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